Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class MainActivity : AppCompatActivity() {
apiKey = "<YOUR API KEY GOES HERE>",
apiUrl = "https://api.bitdrift.io".toHttpUrl(),
sessionStrategy = SessionStrategy.Fixed(),
configuration = Configuration(enableFatalIssueReporting = true),
configuration = Configuration(enableFatalIssueReporting = true, enableNativeCrashReporting = true),
)

if (BuildConfig.DEBUG) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import io.bitdrift.capture.providers.DateProvider
import io.bitdrift.capture.providers.FieldProvider
import io.bitdrift.capture.providers.SystemDateProvider
import io.bitdrift.capture.providers.session.SessionStrategy
import io.bitdrift.capture.reports.FatalIssueReporter
import io.bitdrift.capture.reports.FatalIssueReporterFactory
import io.bitdrift.capture.reports.IFatalIssueReporter
import okhttp3.HttpUrl
import java.util.UUID
Expand Down Expand Up @@ -525,12 +525,7 @@ object Capture {
CaptureJniLibrary.load()
}

val fatalIssueReporter: IFatalIssueReporter? =
if (configuration.enableFatalIssueReporting) {
FatalIssueReporter()
} else {
null
}
val fatalIssueReporter: IFatalIssueReporter? = FatalIssueReporterFactory.create(configuration)

val (loggerImpl, loggerImplBuildDuration) =
measureTimedValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ import io.bitdrift.capture.replay.SessionReplayConfiguration
* @param sessionReplayConfiguration The resource reporting configuration to use. Passing `null` disables the feature.
* @param enableFatalIssueReporting When set to true wil capture Fatal Issues automatically [JVM crash, ANR, etc] and without requiring
* any external 3rd party library integration
* @param enableNativeCrashReporting When set to true will capture native NDK crashes automatically.
* Requires enableFatalIssueReporting to be true. Note: This is a temporary flag and may be removed in future versions.
* @param sleepMode SleepMode.ACTIVE if Capture should initialize in minimal activity mode
*/
data class Configuration
@JvmOverloads
constructor(
val sessionReplayConfiguration: SessionReplayConfiguration = SessionReplayConfiguration(),
val enableFatalIssueReporting: Boolean = false,
val enableNativeCrashReporting: Boolean = false,
val sleepMode: SleepMode = SleepMode.INACTIVE,
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,14 @@ import kotlin.time.measureTime

/**
* Handles internal reporting of crashes
* @param enableNativeCrashReporting Flag to enable native NDK crash reporting.
* Note: This is a temporary flag that may be removed in the future.
* @param backgroundThreadHandler Handler for background thread operations
* @param latestAppExitInfoProvider Provider for retrieving latest app exit information
* @param captureUncaughtExceptionHandler Handler for uncaught exceptions
*/
internal class FatalIssueReporter(
private val enableNativeCrashReporting: Boolean = false,
private val backgroundThreadHandler: IBackgroundThreadHandler = CaptureDispatchers.CommonBackground,
private val latestAppExitInfoProvider: ILatestAppExitInfoProvider = LatestAppExitInfoProvider,
private val captureUncaughtExceptionHandler: ICaptureUncaughtExceptionHandler = CaptureUncaughtExceptionHandler,
Expand Down Expand Up @@ -145,6 +151,7 @@ internal class FatalIssueReporter(
mapToFatalIssueType(lastReason.reason)?.let { fatalIssueType ->
fatalIssueReporterProcessor.persistAppExitReport(
fatalIssueType = fatalIssueType,
enableNativeCrashReporting = enableNativeCrashReporting,
timestamp = lastReason.timestamp,
description = lastReason.description,
traceInputStream = it,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

package io.bitdrift.capture.reports

import io.bitdrift.capture.Configuration

/**
* Factory utility for creating FatalIssueReporter instances based on configuration
*/
internal object FatalIssueReporterFactory {
/**
* Creates a FatalIssueReporter instance based on the provided configuration.
*
* @param configuration The capture configuration containing fatal issue reporting settings
* @return FatalIssueReporter instance if enableFatalIssueReporting is true, null otherwise
*/
fun create(configuration: Configuration): IFatalIssueReporter? =
if (configuration.enableFatalIssueReporting) {
FatalIssueReporter(configuration.enableNativeCrashReporting)
} else {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@ internal class FatalIssueReporterProcessor(
) {
/**
* Process AppTerminations due to ANRs and native crashes into packed format
* @param fatalIssueType The flatbuffer type of fatal issue being processed
* (e.g. [ReportType.AppNotResponding] or [ReportType.NativeCrash])
* @param enableNativeCrashReporting Flag indicating if native crash reporting is enabled.
* Note: This is a temporary flag which may be deleted in the future.
* @param timestamp The timestamp when the issue occurred
* @param description Optional description of the issue
* @param traceInputStream Input stream containing the fatal issue trace data
*/
fun persistAppExitReport(
fatalIssueType: Byte,
enableNativeCrashReporting: Boolean,
timestamp: Long,
description: String? = null,
traceInputStream: InputStream,
Expand All @@ -47,8 +55,8 @@ internal class FatalIssueReporterProcessor(
val deviceMetrics = createDeviceMetrics(builder, timestamp)

val report: Int? =
when (fatalIssueType) {
ReportType.AppNotResponding -> {
when {
fatalIssueType == ReportType.AppNotResponding -> {
AppExitAnrTraceProcessor.process(
builder,
sdk,
Expand All @@ -59,9 +67,15 @@ internal class FatalIssueReporterProcessor(
)
}

ReportType.NativeCrash -> {
// TODO(FranAguilera): BIT-5823 use NativeCrashProcessor once async processing is ready
null
fatalIssueType == ReportType.NativeCrash && enableNativeCrashReporting -> {
NativeCrashProcessor.process(
builder,
sdk,
appMetrics,
deviceMetrics,
description,
traceInputStream,
)
}

else -> null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// capture-sdk - bitdrift's client SDK
// Copyright Bitdrift, Inc. All rights reserved.
//
// Use of this source code is governed by a source available license that can be found in the
// LICENSE file or at:
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt

package io.bitdrift.capture.reports

import io.bitdrift.capture.Configuration
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class FatalIssueReporterFactoryTest {
@Test
fun create_withFatalIssueReportingDisabled_shouldReturnNull() {
val configuration =
Configuration(
enableFatalIssueReporting = false,
enableNativeCrashReporting = false,
)

val reporter = FatalIssueReporterFactory.create(configuration)

assertThat(reporter).isNull()
}

@Test
fun create_withFatalIssueReportingDisabledAndNativeCrashEnabled_shouldReturnNull() {
val configuration =
Configuration(
enableFatalIssueReporting = false,
enableNativeCrashReporting = true,
)

val reporter = FatalIssueReporterFactory.create(configuration)

assertThat(reporter).isNull()
}

@Test
fun create_withFatalIssueReportingEnabledAndNativeCrashDisabled_shouldReturnReporter() {
val configuration =
Configuration(
enableFatalIssueReporting = true,
enableNativeCrashReporting = false,
)

val reporter = FatalIssueReporterFactory.create(configuration)

assertThat(reporter).isInstanceOf(FatalIssueReporter::class.java)
}

@Test
fun create_withBothFatalIssueReportingAndNativeCrashEnabled_shouldReturnReporter() {
val configuration =
Configuration(
enableFatalIssueReporting = true,
enableNativeCrashReporting = true,
)

val reporter = FatalIssueReporterFactory.create(configuration)

assertThat(reporter).isInstanceOf(FatalIssueReporter::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class FatalIssueReporterTest {

private fun buildReporter(): FatalIssueReporter =
FatalIssueReporter(
enableNativeCrashReporting = true,
FakeBackgroundThreadHandler(),
latestAppExitInfoProvider,
captureUncaughtExceptionHandler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import io.bitdrift.capture.reports.binformat.v1.ReportType
import io.bitdrift.capture.reports.persistence.IFatalIssueReporterStorage
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verifyNoInteractions
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.io.InputStream
Expand Down Expand Up @@ -130,6 +130,7 @@ class FatalIssueReporterProcessorTest {

fatalIssueReporterProcessor.persistAppExitReport(
fatalIssueType = ReportType.AppNotResponding,
enableNativeCrashReporting = true,
FAKE_TIME_STAMP,
description,
traceInputStream,
Expand Down Expand Up @@ -323,14 +324,30 @@ class FatalIssueReporterProcessorTest {
)
}

@Ignore("TODO(FranAguilera): BIT-5823 use NativeCrashProcessor once async processing is ready")
@Test
fun persistAppExitReport_whenNativeCrash_shouldCreateEmptyErrorModel() {
fun persistAppExitReport_whenNativeCrashAndNdkProcessingNotConfigured_shouldNotCreateNativeReport() {
val description = "Native crash"
val traceInputStream = buildTraceInputStringFromFile("app_exit_native_crash.bin")

fatalIssueReporterProcessor.persistAppExitReport(
ReportType.NativeCrash,
enableNativeCrashReporting = false,
FAKE_TIME_STAMP,
description,
traceInputStream,
)

verifyNoInteractions(fatalIssueReporterStorage)
}

@Test
fun persistAppExitReport_whenNativeCrashAndNdkProcessingConfigured_shouldCreateNativeReport() {
val description = "Native crash"
val traceInputStream = buildTraceInputStringFromFile("app_exit_native_crash.bin")

fatalIssueReporterProcessor.persistAppExitReport(
ReportType.NativeCrash,
enableNativeCrashReporting = true,
FAKE_TIME_STAMP,
description,
traceInputStream,
Expand Down Expand Up @@ -379,6 +396,7 @@ class FatalIssueReporterProcessorTest {

fatalIssueReporterProcessor.persistAppExitReport(
ReportType.JVMCrash,
enableNativeCrashReporting = true,
FAKE_TIME_STAMP,
description,
traceInputStream,
Expand All @@ -394,6 +412,7 @@ class FatalIssueReporterProcessorTest {
) {
fatalIssueReporterProcessor.persistAppExitReport(
fatalIssueType = ReportType.AppNotResponding,
enableNativeCrashReporting = true,
FAKE_TIME_STAMP,
descriptionFromAppExit,
buildTraceInputStringFromFile("app_exit_anr_deadlock_anr.txt"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import android.content.IntentFilter
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.system.Os
import android.system.OsConstants
import android.util.Log
import androidx.core.content.ContextCompat
import io.bitdrift.capture.Capture
Expand Down Expand Up @@ -111,12 +113,20 @@ internal object FatalIssueGenerator {
}
}

fun forceNativeCrash() {
fun forceCaptureNativeCrash() {
val logger = Capture.logger()
CaptureJniLibrary.destroyLogger((logger as LoggerImpl).loggerId)
Logger.logInfo { "Forced native crash" }
}

fun forceNativeSegmentationFault() {
triggerOsKill(OsConstants.SIGSEGV)
}

fun forceNativeBusError() {
triggerOsKill(OsConstants.SIGBUS)
}

fun forceOutOfMemoryCrash() {
if (oomList.isNotEmpty()) {
return
Expand Down Expand Up @@ -162,6 +172,10 @@ internal object FatalIssueGenerator {
mainThreadHandler.post(action)
}

private fun triggerOsKill(signal: Int) {
Os.kill(Os.getpid(), signal)
}

private val FIRST_LOCK_RESOURCE: Any = "first_lock"
private val SECOND_LOCK_RESOURCE: Any = "second_lock"
private val TAG_NAME = "FatalIssueGenerator"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,10 @@ class FirstFragment : Fragment() {
AppExitReason.APP_CRASH_COROUTINE_EXCEPTION -> FatalIssueGenerator.forceCoroutinesCrash()
AppExitReason.APP_CRASH_REGULAR_JVM_EXCEPTION -> FatalIssueGenerator.forceUnhandledException()
AppExitReason.APP_CRASH_RX_JAVA_EXCEPTION -> FatalIssueGenerator.forceRxJavaException()
AppExitReason.APP_CRASH_NATIVE -> FatalIssueGenerator.forceNativeCrash()
AppExitReason.APP_CRASH_OUT_OF_MEMORY -> FatalIssueGenerator.forceOutOfMemoryCrash()
AppExitReason.NATIVE_CAPTURE_DESTROY_CRASH -> FatalIssueGenerator.forceCaptureNativeCrash()
AppExitReason.NATIVE_SEGMENTATION_FAULT_CRASH -> FatalIssueGenerator.forceNativeSegmentationFault()
AppExitReason.NATIVE_NATIVE_BUS_CRASH -> FatalIssueGenerator.forceNativeBusError()
AppExitReason.SYSTEM_EXIT -> exitProcess(0)
}
}
Expand Down Expand Up @@ -325,7 +327,13 @@ class FirstFragment : Fragment() {
APP_CRASH_REGULAR_JVM_EXCEPTION,
APP_CRASH_RX_JAVA_EXCEPTION,
APP_CRASH_OUT_OF_MEMORY,
APP_CRASH_NATIVE,

NATIVE_CAPTURE_DESTROY_CRASH,

NATIVE_SEGMENTATION_FAULT_CRASH,

NATIVE_NATIVE_BUS_CRASH,

SYSTEM_EXIT,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,15 @@ class GradleTestApp : Application() {
Log.e("GradleTestApp", "Failed to initialize bitdrift logger due to invalid API URL: $stringApiUrl")
return
}

val enableFatalIssueReporting = fatalIssueMechanism == FatalIssueMechanism.BuiltIn.displayName
val configuration = Configuration(enableFatalIssueReporting = enableFatalIssueReporting)
val configuration =
if (fatalIssueMechanism == FatalIssueMechanism.BuiltIn.displayName) {
Configuration(
enableFatalIssueReporting = true,
enableNativeCrashReporting = true,
)
} else {
Configuration()
}
BitdriftInit.initBitdriftCaptureInJava(
apiUrl,
sharedPreferences.getString(BITDRIFT_API_KEY, ""),
Expand Down
Loading