Skip to content

Commit db6248f

Browse files
Copilotmurki
andauthored
BIT-7817: implement skeleton crash reports for null traceInputStream on API level 30
Agent-Logs-Url: https://github.com/bitdriftlabs/capture-sdk/sessions/1eea7ae0-62e8-43c2-92b3-c17d8583429c Co-authored-by: murki <216735+murki@users.noreply.github.com>
1 parent 745a26b commit db6248f

File tree

6 files changed

+90
-8
lines changed

6 files changed

+90
-8
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
**Fixed**
3131

32+
- Fixed an issue where native crashes on API level 30 were silently discarded due to a missing tombstone trace. Skeleton crash reports with basic metadata but no stack traces are now generated in this case.
33+
3234
- Fixed an issue where 3rd-party dependencies like `androidx` were included in the released Javadoc artifacts. Also updated the style of `capture-timber` and `capture-apollo` javadocs to match.
3335

3436
### iOS

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/IssueReporter.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,21 @@ internal class IssueReporter(
165165
val lastReasonResult = latestAppExitInfoProvider.get()
166166
if (lastReasonResult is LatestAppExitReasonResult.Valid) {
167167
val lastReason = lastReasonResult.applicationExitInfo
168-
lastReason.traceInputStream?.use {
169-
latestAppExitInfoProvider.convertExitReasonToFbsReportType(lastReason.reason)?.let { fatalIssueType ->
168+
latestAppExitInfoProvider.convertExitReasonToFbsReportType(lastReason.reason)?.let { fatalIssueType ->
169+
val traceInputStream = lastReason.traceInputStream
170+
traceInputStream?.use {
170171
issueReporterProcessor?.processAppExitReport(
171172
fatalIssueType = fatalIssueType,
172173
timestamp = lastReason.timestamp,
173174
description = lastReason.description,
174175
traceInputStream = it,
175176
)
176-
}
177+
} ?: issueReporterProcessor?.processAppExitReport(
178+
fatalIssueType = fatalIssueType,
179+
timestamp = lastReason.timestamp,
180+
description = lastReason.description,
181+
traceInputStream = null,
182+
)
177183
}
178184
}
179185
}

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/processor/IIssueReporterProcessor.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ interface IIssueReporterProcessor {
2424
* (e.g. [ReportType.AppNotResponding] or [ReportType.NativeCrash]).
2525
* @param timestamp The timestamp when the issue occurred.
2626
* @param description Optional description of the issue.
27-
* @param traceInputStream Input stream containing the fatal issue trace data.
27+
* @param traceInputStream Input stream containing the fatal issue trace data. May be null for
28+
* native crashes on API level 30 where tombstone data is unavailable; a skeleton report will
29+
* be generated in that case.
2830
*/
2931
fun processAppExitReport(
3032
fatalIssueType: Byte,
3133
timestamp: Long,
3234
description: String? = null,
33-
traceInputStream: InputStream,
35+
traceInputStream: InputStream? = null,
3436
)
3537

3638
/**

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,15 @@ internal class IssueReporterProcessor(
100100
* (e.g. [ReportType.AppNotResponding] or [ReportType.NativeCrash])
101101
* @param timestamp The timestamp when the issue occurred
102102
* @param description Optional description of the issue
103-
* @param traceInputStream Input stream containing the fatal issue trace data
103+
* @param traceInputStream Input stream containing the fatal issue trace data. May be null for
104+
* native crashes on API level 30 where tombstone data is unavailable; a skeleton report will
105+
* be generated in that case.
104106
*/
105107
override fun processAppExitReport(
106108
fatalIssueType: Byte,
107109
timestamp: Long,
108110
description: String?,
109-
traceInputStream: InputStream,
111+
traceInputStream: InputStream?,
110112
) {
111113
runCatching {
112114
if (fatalIssueType == ReportType.AppNotResponding) {

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ internal object NativeCrashProcessor {
4141
appMetrics: Int,
4242
deviceMetrics: Int,
4343
description: String?,
44-
traceInputStream: InputStream,
44+
traceInputStream: InputStream?,
4545
): Int {
46+
if (traceInputStream == null) {
47+
return createSkeletonReport(builder, sdk, appMetrics, deviceMetrics, description)
48+
}
49+
4650
val tombstone = Tombstone.parseFrom(traceInputStream)
4751
val nativeErrors = mutableListOf<Int>()
4852
val threadOffsets = mutableListOf<Int>()
@@ -139,6 +143,43 @@ internal object NativeCrashProcessor {
139143
)
140144
}
141145

146+
private fun createSkeletonReport(
147+
builder: FlatBufferBuilder,
148+
sdk: Int,
149+
appMetrics: Int,
150+
deviceMetrics: Int,
151+
description: String?,
152+
): Int {
153+
val name = builder.createString(description ?: "Native crash")
154+
val reason = builder.createString("Native crash")
155+
val errorOffset =
156+
Error.createError(
157+
builder,
158+
name,
159+
reason,
160+
Error.createStackTraceVector(builder, intArrayOf()),
161+
ErrorRelation.CausedBy,
162+
)
163+
val threadDetailsOffset =
164+
ThreadDetails.createThreadDetails(
165+
builder,
166+
0.toUShort(),
167+
ThreadDetails.createThreadsVector(builder, intArrayOf()),
168+
)
169+
return Report.createReport(
170+
builder,
171+
sdk,
172+
ReportType.NativeCrash,
173+
appMetrics,
174+
deviceMetrics,
175+
Report.createErrorsVector(builder, intArrayOf(errorOffset)),
176+
threadDetailsOffset,
177+
Report.createBinaryImagesVector(builder, intArrayOf()),
178+
stateOffset = 0,
179+
featureFlagsOffset = 0,
180+
)
181+
}
182+
142183
private fun createErrorOffset(
143184
builder: FlatBufferBuilder,
144185
description: String?,

platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/reports/processor/IssueReporterProcessorTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,35 @@ class IssueReporterProcessorTest {
275275
assertThat(deviceMetrics?.cpuAbis(0)).isEqualTo("armeabi-v7a")
276276
}
277277

278+
@Test
279+
fun processAppExitReport_whenNativeCrashWithNullTrace_shouldCreateSkeletonNativeReport() {
280+
val description = "Segmentation fault"
281+
282+
processor.processAppExitReport(
283+
ReportType.NativeCrash,
284+
FAKE_TIME_STAMP,
285+
description,
286+
traceInputStream = null,
287+
)
288+
289+
verify(issueReporterStorage).persistFatalIssue(
290+
eq(FAKE_TIME_STAMP),
291+
issueReportCaptor.capture(),
292+
reportTypeCaptor.capture(),
293+
)
294+
val buffer = ByteBuffer.wrap(issueReportCaptor.firstValue)
295+
val report = Report.getRootAsReport(buffer)
296+
assertThat(report.errorsLength).isEqualTo(1)
297+
assertThat(reportTypeCaptor.firstValue).isEqualTo(ReportType.NativeCrash)
298+
299+
val capturedError = report.errors(0)!!
300+
assertThat(capturedError.name).isEqualTo(description)
301+
assertThat(capturedError.reason).isEqualTo("Native crash")
302+
assertThat(capturedError.stackTraceLength).isEqualTo(0)
303+
assertThat(report.threadDetails?.threadsLength).isEqualTo(0)
304+
assertThat(report.binaryImagesLength).isEqualTo(0)
305+
}
306+
278307
@Test
279308
fun processAppExitReport_withInvalidReason_shouldNotInteractWithStorage() {
280309
val description = null

0 commit comments

Comments
 (0)