Skip to content

BIT-7817: Skeleton crash reports for native crashes on API level 30#934

Open
Copilot wants to merge 11 commits intomainfrom
copilot/implement-skeleton-crash-reports
Open

BIT-7817: Skeleton crash reports for native crashes on API level 30#934
Copilot wants to merge 11 commits intomainfrom
copilot/implement-skeleton-crash-reports

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 8, 2026

Adds support for generating "skeleton" native-crash reports on Android API 30 when ApplicationExitInfo.getTraceInputStream() returns null, avoiding silently dropping those crashes.

Test report on Android OS 11 in staging: https://explorations.bitdrift.dev/issues/2274956664977278374/2a26f7f0-c01c-4887-a17c-817fcee6dd50

Changes

  • Make processAppExitReport(..., traceInputStream) nullable through IIssueReporterProcessor/IssueReporterProcessor, removing the = null default so callers must pass the stream explicitly.
  • Update IssueReporter to always invoke .use {} on the (nullable) trace stream and forward it to the processor.
  • Add NativeCrashProcessor fallback to build a minimal native-crash Report when tombstone data is unavailable.
  • Introduce a consolidated SignalInfo data class in NativeCrashProcessor with a single signals map (keyed by signal number) to resolve signal numbers (from ApplicationExitInfo.getStatus()) to signal names (e.g., SIGSEGV) and descriptions (e.g., "Segmentation violation (invalid memory reference)") for skeleton report error name/reason fields. A derived signalsByName map supports lookups from tombstone data.
  • Thread signalNumber parameter through IssueReporterprocessAppExitReportNativeCrashProcessor.createSkeletonReport.
  • Falls back to description/"Native crash" when the signal number is unknown.
  • Make ANR processing a no-op when traceInputStream is null, instead of using a non-null assertion.
  • Add regression tests for skeleton reports with known and unknown signal numbers, plus changelog entry.

  • CHANGELOG.md's "Unreleased" section has been updated, if applicable.
Original prompt

Issue Title: Implement skeleton crash reports without stack traces for native crashs on api level 30
Issue Description: @miguel said:

but going back to my proposal, if they're ok with it as a quick 1st iteration, we could do a very simple change where we still issue some skeleton crash reports with all the basic info but just no stack traces. The Android API we use does report that a native crash happens, it just doesn't contain the tombstone information in it. it should just be a matter of handling a null traceInputStream here: https://github.com/bitdriftlabs/capture-sdk/blob/4ef83ef1273233fa109b87b71360f7a53[…]re/src/main/kotlin/io/bitdrift/capture/reports/IssueReporter.kt

https://github.com/bitdriftlabs/capture-sdk/blob/4ef83ef1273233fa109b87b71360f7a5340b6c38/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/IssueReporter.kt|IssueReporter.kt

            lastReason.traceInputStream?.use {

Screenshot 2026-04-08 at 5.24.49 PM.png

Screenshot 2026-04-08 at 5.25.36 PM.png
Fixes https://linear.app/bitdrift/issue/BIT-7817/implement-skeleton-crash-reports-without-stack-traces-for-native
Branch Name should include the identifier "BIT-7817"

Issue Comments:
Comment by User (2026-04-08T21:40:55.455Z):
This thread is for an agent session with githubcopilot.

Comment by User 408d68e0-7856-4458-9142-c92987d4a514 (2026-04-08T21:40:26.685Z):
This comment thread is synced to a corresponding thread in Slack. All replies are displayed in both locations.

Copilot AI changed the title [WIP] Implement skeleton crash reports without stack traces for native crashes BIT-7817: Skeleton crash reports for native crashes on API level 30 Apr 8, 2026
Copilot finished work on behalf of murki April 8, 2026 21:54
Copilot AI requested a review from murki April 8, 2026 21:54
@murki murki requested a review from Copilot April 8, 2026 21:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds support for generating “skeleton” native-crash reports on Android API 30 when ApplicationExitInfo.getTraceInputStream() returns null, avoiding silently dropping those crashes.

Changes:

  • Make processAppExitReport(..., traceInputStream) nullable through IIssueReporterProcessor/IssueReporterProcessor.
  • Update IssueReporter to always invoke .use {} on the (nullable) trace stream and forward it to the processor.
  • Add NativeCrashProcessor fallback to build a minimal native-crash Report when tombstone data is unavailable, plus a regression test and changelog entry.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/IssueReporter.kt Routes app-exit processing through a single .use {} path that can forward a null stream.
platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/processor/IIssueReporterProcessor.kt Updates API contract to accept a nullable trace stream (defaulting to null).
platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/processor/IssueReporterProcessor.kt Propagates nullable stream into native crash processing and ANR processing path.
platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/processor/NativeCrashProcessor.kt Generates a skeleton native crash report when tombstone stream is null.
platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/reports/processor/IssueReporterProcessorTest.kt Adds test coverage for the new skeleton-report behavior.
CHANGELOG.md Documents the API-30 native-crash reporting fix.
Comments suppressed due to low confidence (1)

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/processor/IssueReporterProcessor.kt:117

  • traceInputStream is now nullable, but the ANR branch passes it directly to processAndPersistANR(stream: InputStream, ...), which requires a non-null stream. This is a compile-time type mismatch (and would also be a runtime issue if forced). Consider requiring a non-null stream when fatalIssueType == ReportType.AppNotResponding (e.g., val stream = requireNotNull(traceInputStream) with a clear error/log + early return) while keeping null allowed for native crashes.
        traceInputStream: InputStream?,
    ) {
        runCatching {
            if (fatalIssueType == ReportType.AppNotResponding) {
                streamingReportsProcessor.processAndPersistANR(
                    traceInputStream,
                    timestamp,

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot stopped work on behalf of murki due to an error April 8, 2026 22:06
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

Size Comparison Report (x86_64)

Metric APK (KB) SO (KB)
Baseline 3406 1280
Current 3411 1280
Difference 5 0

APK size increased by 5 KB. SO size unchanged.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 8, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • dl.google.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED -Xmx2048M -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant -cp /home/REDACTED/.gradle/wrapper/dists/gradle-8.14.3-bin/cv11ve7ro1n3o1j4so8xd9n66/gradle-8.14.3/lib/gradle-daemon-main-8.14.3.jar (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of murki April 8, 2026 22:30
Copilot AI requested a review from murki April 8, 2026 22:30
Copilot finished work on behalf of murki April 8, 2026 22:46
Copilot AI requested a review from murki April 8, 2026 22:46
Copilot AI requested a review from murki April 8, 2026 22:51
Copilot finished work on behalf of murki April 8, 2026 22:51
@murki
Copy link
Copy Markdown
Contributor

murki commented Apr 8, 2026

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 8, 2026

@copilot apply changes based on the comments in this thread

Removed the = null default from traceInputStream in IIssueReporterProcessor so callers must pass it explicitly. All existing call sites already pass it explicitly. See 64ccb05.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • dl.google.com
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java /usr/lib/jvm/temurin-17-jdk-amd64/bin/java --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.prefs/java.util.prefs=ALL-UNNAMED --add-opens=java.base/java.nio.charset=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED --add-opens=java.xml/javax.xml.namespace=ALL-UNNAMED -Xmx2048M -Dfile.encoding=UTF-8 -Duser.country -Duser.language=en -Duser.variant -cp /home/REDACTED/.gradle/wrapper/dists/gradle-8.14.3-bin/cv11ve7ro1n3o1j4so8xd9n66/gradle-8.14.3/lib/gradle-daemon-main-8.14.3.jar (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@murki murki marked this pull request as ready for review April 8, 2026 23:06
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/processor/NativeCrashProcessor.kt:206

  • description is nullable, but signalName.ifEmpty { description } requires a non-null fallback string. As written, this won’t compile (or would risk passing null into createString). Use a non-null default (e.g., description ?: "Native crash") for the empty-signal case so the error name/reason is always a valid string.
        tombstone: Tombstone,
        frameOffsets: IntArray,
    ): Int {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +282 to +288
processor.processAppExitReport(
ReportType.NativeCrash,
FAKE_TIME_STAMP,
description,
traceInputStream = null,
signalNumber = 11, // SIGSEGV
)
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because IssueReporterProcessor.processAppExitReport(...) now requires signalNumber (no default on the concrete override), several existing calls in this same test file still invoke it with only 4 arguments (e.g., the ANR and existing native-crash tests). Those call sites will no longer compile unless you either (a) update them to pass signalNumber = 0, or (b) add a default value for signalNumber on the override as well (keeping in mind defaults are resolved by the static type).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

description: String?,
traceInputStream: InputStream,
traceInputStream: InputStream?,
signalNumber: Int,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

signalNumber has no default value in the concrete IssueReporterProcessor.processAppExitReport override, but several call sites (including existing tests in this module) invoke processAppExitReport(...) without providing it. In Kotlin, default parameters from the interface are only applied when calling through the interface type, so these calls will fail to compile when the receiver is IssueReporterProcessor. Consider giving signalNumber a default of 0 in the override as well (or update all call sites to pass an explicit value).

Suggested change
signalNumber: Int,
signalNumber: Int = 0,

Copilot uses AI. Check for mistakes.
Comment on lines +168 to 177
latestAppExitInfoProvider.convertExitReasonToFbsReportType(lastReason.reason)?.let { fatalIssueType ->
lastReason.traceInputStream.use { traceInputStream ->
issueReporterProcessor?.processAppExitReport(
fatalIssueType = fatalIssueType,
timestamp = lastReason.timestamp,
description = lastReason.description,
traceInputStream = it,
traceInputStream = traceInputStream,
signalNumber = lastReason.status,
)
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

traceInputStream is only wrapped in .use {} after convertExitReasonToFbsReportType(...) succeeds. If the exit reason maps to null but traceInputStream is non-null, the stream will never be closed. To avoid leaking the underlying FD/stream, wrap the .use {} around the stream unconditionally (and then decide inside the block whether to call processAppExitReport).

Copilot uses AI. Check for mistakes.
Comment on lines 207 to 212
val signalName = tombstone.signalInfo.name
val reason = builder.createString(signalName.ifEmpty { description })
val causeText =
tombstone.causesList.firstOrNull()?.humanReadable ?: tombstone.abortMessage.ifEmpty {
signalDescriptions[signalName] ?: "Native crash"
signalsByName[signalName]?.description ?: "Native crash"
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createErrorOffset can pass null into FlatBufferBuilder.createString(...) when tombstone.signalInfo.name is empty and description is null (signalName.ifEmpty { description }). Because createString is a Java API (platform type), this compiles but can throw at runtime and cause the native crash report to be dropped. Ensure a non-null fallback string is always used (e.g., fall back to "Native crash" when both are missing).

Copilot uses AI. Check for mistakes.
Comment on lines 30 to +33
**Fixed**

- Fatal Issue Reports are now being sent on API level 30. Crash reports will have basic metadata but no stack traces.

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog entry says "Fatal Issue Reports are now being sent on API level 30", but the PR changes appear specific to native crashes where ApplicationExitInfo.getTraceInputStream() is null (skeleton native-crash reports). Consider rewording to avoid implying all fatal issue reports were previously not sent on API 30, and to call out that these are native crash reports without tombstone/stack traces.

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

Android Benchmark Results

Device: PR: Emulator - Android SDK built for x86_64 (API 31) | main: Emulator - Android SDK built for x86_64 (API 31)

Note: Benchmarks run on emulator. Results may vary between runs and may differ from physical devices.

Allocations

Test PR main Δ
logHttpNetworkLog50FieldsAndHeadersAndFieldProviders 923 924 -0.1%
logNotMatched5000Fields 10 10 0.0%
logNotMatchedNoFields 1 1 0.0%
trackSpansWithFields 140 140 0.0%
trackSpansWithoutFields 55 55 0.0%
webViewBridgeBridgeReady 60 61 -1.6%
webViewBridgeCustomLog 59 66 -10.6%
webViewBridgeInvalidJson 38 38 0.0%
webViewBridgeWebVitalCLS 177 178 -0.6%

Timing

Test PR main Δ
logHttpNetworkLog50FieldsAndHeadersAndFieldProviders 238.24 us 235.02 us +1.4%
logNotMatched5000Fields 2.41 ms 2.44 ms -1.0%
logNotMatchedNoFields 458 ns 469 ns -2.3%
trackSpansWithFields 476.76 us 473.52 us +0.7%
trackSpansWithoutFields 7.82 us 7.84 us -0.3%
webViewBridgeBridgeReady 8.01 us 8.16 us -1.8%
webViewBridgeCustomLog 9.26 us 9.26 us 0.0%
webViewBridgeInvalidJson 62.16 us 120.55 us -48.4%
webViewBridgeWebVitalCLS 20.79 us 20.08 us +3.6%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants