diff --git a/.gitignore b/.gitignore index 5f772be1b27..b8c2d7e9da2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +.java-version .idea/ .gradle/ .run/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd6e7eecab..d538f8f4453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Update Android targetSdk to API 36 (Android 16) ([#5016](https://github.com/getsentry/sentry-java/pull/5016)) +- Merge Tombstone and Native SDK events into single crash event. ([#5037](https://github.com/getsentry/sentry-java/pull/5037)) ### Internal diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index ff9a0c7597d..aa583d76b38 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -291,6 +291,21 @@ public final class io/sentry/android/core/LoadClass : io/sentry/util/LoadClass { public fun loadClass (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/lang/Class; } +public final class io/sentry/android/core/NativeEventCollector { + public fun (Lio/sentry/android/core/SentryAndroidOptions;)V + public fun collect ()V + public fun deleteNativeEventFile (Lio/sentry/android/core/NativeEventCollector$NativeEventData;)Z + public fun findAndRemoveMatchingNativeEvent (JLjava/lang/String;)Lio/sentry/android/core/NativeEventCollector$NativeEventData; +} + +public final class io/sentry/android/core/NativeEventCollector$NativeEventData { + public fun getCorrelationId ()Ljava/lang/String; + public fun getEnvelope ()Lio/sentry/SentryEnvelope; + public fun getEvent ()Lio/sentry/SentryEvent; + public fun getFile ()Ljava/io/File; + public fun getTimestampMs ()J +} + public final class io/sentry/android/core/NdkHandlerStrategy : java/lang/Enum { public static final field SENTRY_HANDLER_STRATEGY_CHAIN_AT_START Lio/sentry/android/core/NdkHandlerStrategy; public static final field SENTRY_HANDLER_STRATEGY_DEFAULT Lio/sentry/android/core/NdkHandlerStrategy; @@ -339,6 +354,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback; public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader; public fun getFrameMetricsCollector ()Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector; + public fun getNativeCrashCorrelationId ()Ljava/lang/String; public fun getNativeSdkName ()Ljava/lang/String; public fun getNdkHandlerStrategy ()I public fun getStartupCrashDurationThresholdMillis ()J @@ -392,6 +408,7 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableSystemEventBreadcrumbs (Z)V public fun setEnableSystemEventBreadcrumbsExtras (Z)V public fun setFrameMetricsCollector (Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V + public fun setNativeCrashCorrelationId (Ljava/lang/String;)V public fun setNativeHandlerStrategy (Lio/sentry/android/core/NdkHandlerStrategy;)V public fun setNativeSdkName (Ljava/lang/String;)V public fun setReportHistoricalAnrs (Z)V @@ -500,7 +517,7 @@ public final class io/sentry/android/core/TombstoneIntegration$TombstoneHint : i } public class io/sentry/android/core/TombstoneIntegration$TombstonePolicy : io/sentry/android/core/ApplicationExitInfoHistoryDispatcher$ApplicationExitInfoPolicy { - public fun (Lio/sentry/android/core/SentryAndroidOptions;)V + public fun (Lio/sentry/android/core/SentryAndroidOptions;Landroid/content/Context;)V public fun buildReport (Landroid/app/ApplicationExitInfo;Z)Lio/sentry/android/core/ApplicationExitInfoHistoryDispatcher$Report; public fun getLabel ()Ljava/lang/String; public fun getLastReportedTimestamp ()Ljava/lang/Long; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index b7bb5bf21ac..784546f230a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -2,6 +2,7 @@ import static io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME; +import android.app.ActivityManager; import android.app.Application; import android.content.Context; import android.content.pm.PackageInfo; @@ -57,8 +58,10 @@ import io.sentry.util.Objects; import io.sentry.util.thread.NoOpThreadChecker; import java.io.File; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -244,6 +247,12 @@ static void initializeIntegrationsAndProcessors( if (options.getSocketTagger() instanceof NoOpSocketTagger) { options.setSocketTagger(AndroidSocketTagger.getInstance()); } + + // Set native crash correlation ID before NDK integration is registered + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.R) { + setNativeCrashCorrelationId(context, options); + } + if (options.getPerformanceCollectors().isEmpty()) { options.addPerformanceCollector(new AndroidMemoryCollector()); options.addPerformanceCollector(new AndroidCpuCollector(options.getLogger())); @@ -497,4 +506,33 @@ private static void readDefaultOptionValues( static @NotNull File getCacheDir(final @NotNull Context context) { return new File(context.getCacheDir(), "sentry"); } + + /** + * Sets a native crash correlation ID that can be used to associate native crash events (from + * sentry-native) with tombstone events (from ApplicationExitInfo). The ID is stored via + * ActivityManager.setProcessStateSummary() and passed to the native SDK. + * + * @param context the Application context + * @param options the SentryAndroidOptions + */ + private static void setNativeCrashCorrelationId( + final @NotNull Context context, final @NotNull SentryAndroidOptions options) { + final String correlationId = UUID.randomUUID().toString(); + options.setNativeCrashCorrelationId(correlationId); + + try { + final ActivityManager am = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + if (am != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + am.setProcessStateSummary(correlationId.getBytes(StandardCharsets.UTF_8)); + options + .getLogger() + .log(SentryLevel.DEBUG, "Native crash correlation ID set: %s", correlationId); + } + } catch (Throwable e) { + options + .getLogger() + .log(SentryLevel.WARNING, "Failed to set process state summary for correlation ID", e); + } + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java new file mode 100644 index 00000000000..17379299ea0 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/NativeEventCollector.java @@ -0,0 +1,276 @@ +package io.sentry.android.core; + +import static io.sentry.cache.EnvelopeCache.PREFIX_CURRENT_SESSION_FILE; +import static io.sentry.cache.EnvelopeCache.PREFIX_PREVIOUS_SESSION_FILE; +import static io.sentry.cache.EnvelopeCache.STARTUP_CRASH_MARKER_FILE; + +import io.sentry.SentryEnvelope; +import io.sentry.SentryEnvelopeItem; +import io.sentry.SentryEvent; +import io.sentry.SentryItemType; +import io.sentry.SentryLevel; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Collects native crash events from the outbox directory. These events can be correlated with + * tombstone events from ApplicationExitInfo to avoid sending duplicate crash reports. + */ +@ApiStatus.Internal +public final class NativeEventCollector { + + private static final String NATIVE_PLATFORM = "native"; + + // TODO: will be replaced with the correlationId once the Native SDK supports it + private static final long TIMESTAMP_TOLERANCE_MS = 5000; + + private final @NotNull SentryAndroidOptions options; + private final @NotNull List nativeEvents = new ArrayList<>(); + private boolean collected = false; + + public NativeEventCollector(final @NotNull SentryAndroidOptions options) { + this.options = options; + } + + /** Holds a native event along with its source file for later deletion. */ + public static final class NativeEventData { + private final @NotNull SentryEvent event; + private final @NotNull File file; + private final @NotNull SentryEnvelope envelope; + private final long timestampMs; + + NativeEventData( + final @NotNull SentryEvent event, + final @NotNull File file, + final @NotNull SentryEnvelope envelope, + final long timestampMs) { + this.event = event; + this.file = file; + this.envelope = envelope; + this.timestampMs = timestampMs; + } + + public @NotNull SentryEvent getEvent() { + return event; + } + + public @NotNull File getFile() { + return file; + } + + public @NotNull SentryEnvelope getEnvelope() { + return envelope; + } + + public long getTimestampMs() { + return timestampMs; + } + + /** + * Extracts the correlation ID from the event's extra data. + * + * @return the correlation ID, or null if not present + */ + public @Nullable String getCorrelationId() { + final @Nullable Object correlationId = event.getExtra("sentry.native.correlation_id"); + if (correlationId instanceof String) { + return (String) correlationId; + } + return null; + } + } + + /** + * Scans the outbox directory and collects all native crash events. This method should be called + * once before processing tombstones. Subsequent calls are no-ops. + */ + public void collect() { + if (collected) { + return; + } + collected = true; + + final @Nullable String outboxPath = options.getOutboxPath(); + if (outboxPath == null) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Outbox path is null, skipping native event collection."); + return; + } + + final File outboxDir = new File(outboxPath); + if (!outboxDir.isDirectory()) { + options.getLogger().log(SentryLevel.DEBUG, "Outbox path is not a directory: %s", outboxPath); + return; + } + + final File[] files = outboxDir.listFiles((d, name) -> isRelevantFileName(name)); + if (files == null || files.length == 0) { + options.getLogger().log(SentryLevel.DEBUG, "No envelope files found in outbox."); + return; + } + + options + .getLogger() + .log(SentryLevel.DEBUG, "Scanning %d files in outbox for native events.", files.length); + + for (final File file : files) { + if (!file.isFile()) { + continue; + } + + final @Nullable NativeEventData nativeEventData = extractNativeEventFromFile(file); + if (nativeEventData != null) { + nativeEvents.add(nativeEventData); + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Found native event in outbox: %s (timestamp: %d)", + file.getName(), + nativeEventData.getTimestampMs()); + } + } + + options + .getLogger() + .log(SentryLevel.DEBUG, "Collected %d native events from outbox.", nativeEvents.size()); + } + + /** + * Finds a native event that matches the given tombstone timestamp or correlation ID. If a match + * is found, it is removed from the internal list so it won't be matched again. + * + *

This method will lazily collect native events from the outbox on first call. + * + * @param tombstoneTimestampMs the timestamp from ApplicationExitInfo + * @param correlationId the correlation ID from processStateSummary, or null + * @return the matching native event data, or null if no match found + */ + public @Nullable NativeEventData findAndRemoveMatchingNativeEvent( + final long tombstoneTimestampMs, final @Nullable String correlationId) { + + // Lazily collect on first use (runs on executor thread, not main thread) + collect(); + + // First, try to match by correlation ID (when sentry-native supports it) + if (correlationId != null) { + for (final NativeEventData nativeEvent : nativeEvents) { + final @Nullable String nativeCorrelationId = nativeEvent.getCorrelationId(); + if (correlationId.equals(nativeCorrelationId)) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Matched native event by correlation ID: %s", correlationId); + nativeEvents.remove(nativeEvent); + return nativeEvent; + } + } + } + + // Fall back to timestamp-based matching + for (final NativeEventData nativeEvent : nativeEvents) { + final long timeDiff = Math.abs(tombstoneTimestampMs - nativeEvent.getTimestampMs()); + if (timeDiff <= TIMESTAMP_TOLERANCE_MS) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Matched native event by timestamp (diff: %d ms)", timeDiff); + nativeEvents.remove(nativeEvent); + return nativeEvent; + } + } + + return null; + } + + /** + * Deletes a native event file from the outbox. + * + * @param nativeEventData the native event data containing the file reference + * @return true if the file was deleted successfully + */ + public boolean deleteNativeEventFile(final @NotNull NativeEventData nativeEventData) { + final File file = nativeEventData.getFile(); + try { + if (file.delete()) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Deleted native event file from outbox: %s", file.getName()); + return true; + } else { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Failed to delete native event file: %s", + file.getAbsolutePath()); + return false; + } + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, e, "Error deleting native event file: %s", file.getAbsolutePath()); + return false; + } + } + + private @Nullable NativeEventData extractNativeEventFromFile(final @NotNull File file) { + try (final InputStream stream = new BufferedInputStream(new FileInputStream(file))) { + final SentryEnvelope envelope = options.getEnvelopeReader().read(stream); + if (envelope == null) { + return null; + } + + for (final SentryEnvelopeItem item : envelope.getItems()) { + if (!SentryItemType.Event.equals(item.getHeader().getType())) { + continue; + } + + try (final Reader eventReader = + new BufferedReader( + new InputStreamReader( + new ByteArrayInputStream(item.getData()), StandardCharsets.UTF_8))) { + final SentryEvent event = + options.getSerializer().deserialize(eventReader, SentryEvent.class); + if (event != null && NATIVE_PLATFORM.equals(event.getPlatform())) { + final long timestampMs = extractTimestampMs(event); + return new NativeEventData(event, file, envelope, timestampMs); + } + } + } + } catch (Throwable e) { + options + .getLogger() + .log(SentryLevel.DEBUG, e, "Error reading envelope file: %s", file.getAbsolutePath()); + } + return null; + } + + private long extractTimestampMs(final @NotNull SentryEvent event) { + final @Nullable Date timestamp = event.getTimestamp(); + if (timestamp != null) { + return timestamp.getTime(); + } + return 0; + } + + private boolean isRelevantFileName(final @Nullable String fileName) { + return fileName != null + && !fileName.startsWith(PREFIX_CURRENT_SESSION_FILE) + && !fileName.startsWith(PREFIX_PREVIOUS_SESSION_FILE) + && !fileName.startsWith(STARTUP_CRASH_MARKER_FILE); + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 12917ed4b7c..3baba6a3235 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -174,6 +174,13 @@ public final class SentryAndroidOptions extends SentryOptions { */ private boolean enableScopeSync = true; + /** + * A correlation ID used to associate native crash events (from sentry-native) with tombstone + * events (from ApplicationExitInfo). This is set via ActivityManager.setProcessStateSummary() and + * passed to the native SDK during initialization. + */ + private @Nullable String nativeCrashCorrelationId; + /** * Whether to enable automatic trace ID generation. This is mainly used by the Hybrid SDKs to * control the trace ID generation from the outside. @@ -607,6 +614,27 @@ public void setEnableScopeSync(boolean enableScopeSync) { this.enableScopeSync = enableScopeSync; } + /** + * Returns the correlation ID used to associate native crash events with tombstone events. + * + * @return the correlation ID, or null if not set + */ + @ApiStatus.Internal + public @Nullable String getNativeCrashCorrelationId() { + return nativeCrashCorrelationId; + } + + /** + * Sets the correlation ID used to associate native crash events with tombstone events. This is + * typically set automatically during SDK initialization. + * + * @param nativeCrashCorrelationId the correlation ID + */ + @ApiStatus.Internal + public void setNativeCrashCorrelationId(final @Nullable String nativeCrashCorrelationId) { + this.nativeCrashCorrelationId = nativeCrashCorrelationId; + } + public boolean isReportHistoricalAnrs() { return reportHistoricalAnrs; } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java index 6d1c56db5ee..d6a64fa0cb3 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/TombstoneIntegration.java @@ -15,12 +15,18 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.android.core.ApplicationExitInfoHistoryDispatcher.ApplicationExitInfoPolicy; +import io.sentry.android.core.NativeEventCollector.NativeEventData; import io.sentry.android.core.cache.AndroidEnvelopeCache; +import io.sentry.android.core.internal.tombstone.NativeExceptionMechanism; import io.sentry.android.core.internal.tombstone.TombstoneParser; import io.sentry.hints.Backfillable; import io.sentry.hints.BlockingFlushHint; import io.sentry.hints.NativeCrashExit; +import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.Mechanism; +import io.sentry.protocol.SentryException; import io.sentry.protocol.SentryId; +import io.sentry.protocol.SentryThread; import io.sentry.transport.CurrentDateProvider; import io.sentry.transport.ICurrentDateProvider; import io.sentry.util.HintUtils; @@ -28,8 +34,10 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.format.DateTimeFormatter; +import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -83,7 +91,7 @@ public void register(@NotNull IScopes scopes, @NotNull SentryOptions options) { scopes, this.options, dateProvider, - new TombstonePolicy(this.options))); + new TombstonePolicy(this.options, this.context))); } catch (Throwable e) { options.getLogger().log(SentryLevel.DEBUG, "Failed to start tombstone processor.", e); } @@ -103,9 +111,13 @@ public void close() throws IOException { public static class TombstonePolicy implements ApplicationExitInfoPolicy { private final @NotNull SentryAndroidOptions options; + private final @NotNull NativeEventCollector nativeEventCollector; + @NotNull private final Context context; - public TombstonePolicy(final @NotNull SentryAndroidOptions options) { + public TombstonePolicy(final @NotNull SentryAndroidOptions options, @NotNull Context context) { this.options = options; + this.nativeEventCollector = new NativeEventCollector(options); + this.context = context; } @Override @@ -133,7 +145,7 @@ public boolean shouldReportHistorical() { @Override public @Nullable ApplicationExitInfoHistoryDispatcher.Report buildReport( final @NotNull ApplicationExitInfo exitInfo, final boolean enrich) { - final SentryEvent event; + SentryEvent event; try { final InputStream tombstoneInputStream = exitInfo.getTraceInputStream(); if (tombstoneInputStream == null) { @@ -147,7 +159,12 @@ public boolean shouldReportHistorical() { return null; } - try (final TombstoneParser parser = new TombstoneParser(tombstoneInputStream)) { + try (final TombstoneParser parser = + new TombstoneParser( + tombstoneInputStream, + this.options.getInAppIncludes(), + this.options.getInAppExcludes(), + this.context.getApplicationInfo().nativeLibraryDir)) { event = parser.parse(); } } catch (Throwable e) { @@ -164,6 +181,36 @@ public boolean shouldReportHistorical() { final long tombstoneTimestamp = exitInfo.getTimestamp(); event.setTimestamp(DateUtils.getDateTime(tombstoneTimestamp)); + // Extract correlation ID from process state summary (if set during previous session) + final @Nullable String correlationId = extractCorrelationId(exitInfo); + if (correlationId != null) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Tombstone correlation ID found: %s", correlationId); + } + + // Try to find and remove matching native event from outbox + final @Nullable NativeEventData matchingNativeEvent = + nativeEventCollector.findAndRemoveMatchingNativeEvent(tombstoneTimestamp, correlationId); + + if (matchingNativeEvent != null) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Found matching native event for tombstone, removing from outbox: %s", + matchingNativeEvent.getFile().getName()); + + // Delete from outbox so OutboxSender doesn't send it + boolean deletionSuccess = nativeEventCollector.deleteNativeEventFile(matchingNativeEvent); + + if (deletionSuccess) { + event = mergeNativeCrashes(matchingNativeEvent.getEvent(), event); + } + } else { + options.getLogger().log(SentryLevel.DEBUG, "No matching native event found for tombstone."); + } + final TombstoneHint tombstoneHint = new TombstoneHint( options.getFlushTimeoutMillis(), options.getLogger(), tombstoneTimestamp, enrich); @@ -171,6 +218,54 @@ public boolean shouldReportHistorical() { return new ApplicationExitInfoHistoryDispatcher.Report(event, hint, tombstoneHint); } + + private SentryEvent mergeNativeCrashes( + final @NotNull SentryEvent nativeEvent, final @NotNull SentryEvent tombstoneEvent) { + // we take the event data verbatim from the Native SDK and only apply tombstone data where we + // are sure that it will improve the outcome: + // * context from the Native SDK will be closer to what users want than any backfilling + // * the Native SDK only tracks the crashing thread (vs. tombstone dumps all) + // * even for the crashing we expect a much better stack-trace (+ symbolication) + // * tombstone adds additional exception meta-data to signal handler content + // * we add debug-meta for consistency since the Native SDK caches memory maps early + @Nullable List tombstoneExceptions = tombstoneEvent.getExceptions(); + @Nullable DebugMeta tombstoneDebugMeta = tombstoneEvent.getDebugMeta(); + @Nullable List tombstoneThreads = tombstoneEvent.getThreads(); + if (tombstoneExceptions != null + && !tombstoneExceptions.isEmpty() + && tombstoneDebugMeta != null + && tombstoneThreads != null) { + // native crashes don't nest, we always expect one level. + SentryException exception = tombstoneExceptions.get(0); + @Nullable Mechanism mechanism = exception.getMechanism(); + if (mechanism != null) { + mechanism.setType(NativeExceptionMechanism.TOMBSTONE_MERGED.getValue()); + } + nativeEvent.setExceptions(tombstoneExceptions); + nativeEvent.setDebugMeta(tombstoneDebugMeta); + nativeEvent.setThreads(tombstoneThreads); + } + + return nativeEvent; + } + + @RequiresApi(api = Build.VERSION_CODES.R) + private @Nullable String extractCorrelationId(final @NotNull ApplicationExitInfo exitInfo) { + try { + final byte[] summary = exitInfo.getProcessStateSummary(); + if (summary != null && summary.length > 0) { + return new String(summary, StandardCharsets.UTF_8); + } + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Failed to extract correlation ID from process state summary", + e); + } + return null; + } } @ApiStatus.Internal diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java index 18c10fac445..ae6123bef05 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/tombstone/TombstoneParser.java @@ -21,18 +21,30 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import org.jetbrains.annotations.NotNull; public class TombstoneParser implements Closeable { private final InputStream tombstoneStream; + @NotNull private final List inAppIncludes; + @NotNull private final List inAppExcludes; + // TODO: in theory can be null, but practically not for native crashes + private final String nativeLibraryDir; private final Map excTypeValueMap = new HashMap<>(); private static String formatHex(long value) { return String.format("0x%x", value); } - public TombstoneParser(@NonNull final InputStream tombstoneStream) { + public TombstoneParser( + @NonNull final InputStream tombstoneStream, + @NotNull List inAppIncludes, + @NotNull List inAppExcludes, + String nativeLibraryDir) { this.tombstoneStream = tombstoneStream; + this.inAppIncludes = inAppIncludes; + this.inAppExcludes = inAppExcludes; + this.nativeLibraryDir = nativeLibraryDir; // keep the current signal type -> value mapping for compatibility excTypeValueMap.put("SIGILL", "IllegalInstruction"); @@ -91,14 +103,38 @@ private List createThreads( } @NonNull - private static SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread thread) { + private SentryStackTrace createStackTrace(@NonNull final TombstoneProtos.Thread thread) { final List frames = new ArrayList<>(); for (TombstoneProtos.BacktraceFrame frame : thread.getCurrentBacktraceList()) { + if (frame.getFileName().endsWith("libart.so")) { + // We ignore all ART frames for time being because they aren't actionable for app developers + continue; + } final SentryStackFrame stackFrame = new SentryStackFrame(); stackFrame.setPackage(frame.getFileName()); stackFrame.setFunction(frame.getFunctionName()); stackFrame.setInstructionAddr(formatHex(frame.getPc())); + + // TODO: is this the right order? + boolean inApp = false; + for (String inclusion : this.inAppIncludes) { + if (frame.getFunctionName().startsWith(inclusion)) { + inApp = true; + break; + } + } + + for (String exclusion : this.inAppExcludes) { + if (frame.getFunctionName().startsWith(exclusion)) { + inApp = false; + break; + } + } + + inApp = inApp || frame.getFileName().startsWith(this.nativeLibraryDir); + + stackFrame.setInApp(inApp); frames.add(0, stackFrame); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitIntegrationTestBase.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitIntegrationTestBase.kt index 4f2533d4268..13f9fca1782 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitIntegrationTestBase.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ApplicationExitIntegrationTestBase.kt @@ -54,6 +54,9 @@ abstract class ApplicationExitIntegrationTestBase { @BeforeTest fun `set up`() { val context = ApplicationProvider.getApplicationContext() + // the integration test app has no native library and as such we have to inject one here + context.applicationInfo.nativeLibraryDir = + "/data/app/~~YtXYvdWm5vDHUWYCmVLG_Q==/io.sentry.samples.android-Q2_nG8SyOi4X_6hGGDGE2Q==/lib/arm64" fixture.init(context) } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt index 954ad0eccc6..e346c23e3ee 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/tombstone/TombstoneParserTest.kt @@ -46,11 +46,16 @@ class TombstoneParserTest { "x28", ) + val inAppIncludes = arrayListOf("io.sentry.samples.android") + val inAppExcludes = arrayListOf() + val nativeLibraryDir = + "/data/app/~~YtXYvdWm5vDHUWYCmVLG_Q==/io.sentry.samples.android-Q2_nG8SyOi4X_6hGGDGE2Q==/lib/arm64" + @Test fun `parses a snapshot tombstone into Event`() { val tombstoneStream = GZIPInputStream(TombstoneParserTest::class.java.getResourceAsStream("/tombstone.pb.gz")) - val parser = TombstoneParser(tombstoneStream) + val parser = TombstoneParser(tombstoneStream, inAppIncludes, inAppExcludes, nativeLibraryDir) val event = parser.parse() // top-level data @@ -93,6 +98,15 @@ class TombstoneParserTest { assertNotNull(frame.function) assertNotNull(frame.`package`) assertNotNull(frame.instructionAddr) + + if (thread.id == crashedThreadId) { + if (frame.isInApp!!) { + assert( + frame.function!!.startsWith(inAppIncludes[0]) || + frame.filename!!.startsWith(nativeLibraryDir) + ) + } + } } assert(thread.stacktrace!!.registers!!.keys.containsAll(expectedRegisters)) @@ -160,7 +174,13 @@ class TombstoneParserTest { ) .build() - val parser = TombstoneParser(ByteArrayInputStream(tombstone.toByteArray())) + val parser = + TombstoneParser( + ByteArrayInputStream(tombstone.toByteArray()), + inAppIncludes, + inAppExcludes, + nativeLibraryDir, + ) val event = parser.parse() val images = event.debugMeta!!.images!! diff --git a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java index 9d6d64a1236..ff0fe421f8e 100644 --- a/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java +++ b/sentry-android-ndk/src/main/java/io/sentry/android/ndk/SentryNdk.java @@ -73,6 +73,12 @@ public static void init(@NotNull final SentryAndroidOptions options) { ndkOptions.setTracesSampleRate(tracesSampleRate.floatValue()); } + // TODO: Pass correlation ID to native SDK when sentry-native supports it + // final @Nullable String correlationId = options.getNativeCrashCorrelationId(); + // if (correlationId != null) { + // ndkOptions.setCorrelationId(correlationId); + // } + //noinspection UnstableApiUsage io.sentry.ndk.SentryNdk.init(ndkOptions);