diff --git a/examples/android/HelloWorldApp.kt b/examples/android/HelloWorldApp.kt index 389807041..b35c1724b 100644 --- a/examples/android/HelloWorldApp.kt +++ b/examples/android/HelloWorldApp.kt @@ -12,13 +12,12 @@ import android.os.Handler import android.os.Looper import android.util.Log import io.bitdrift.capture.Capture.Logger +import io.bitdrift.capture.Configuration import io.bitdrift.capture.common.DefaultClock import io.bitdrift.capture.providers.FieldProvider import io.bitdrift.capture.providers.session.SessionStrategy import okhttp3.HttpUrl import java.util.UUID -import io.bitdrift.capture.experimental.ExperimentalBitdriftApi -import io.bitdrift.capture.reports.FatalIssueMechanism private const val bitdriftAPIKey = "" private val BITDRIFT_URL = HttpUrl.Builder().scheme("https").host("api.bitdrift.io").build() @@ -29,9 +28,6 @@ class HelloWorldApp : Application() { override fun onCreate() { super.onCreate() - @OptIn(ExperimentalBitdriftApi::class) - Logger.initFatalIssueReporting(fatalIssueMechanism = FatalIssueMechanism.BuiltIn) - setupExampleCrashHandler() val userID = UUID.randomUUID().toString(); @@ -40,6 +36,7 @@ class HelloWorldApp : Application() { apiKey = bitdriftAPIKey, apiUrl = BITDRIFT_URL, sessionStrategy = SessionStrategy.Fixed { UUID.randomUUID().toString() }, + configuration = Configuration(enableFatalIssueReporting = true), fieldProviders = listOf( FieldProvider { mapOf( diff --git a/examples/swift/hello_world/LoggerCustomer.swift b/examples/swift/hello_world/LoggerCustomer.swift index 6e2edecbb..8bd61e67e 100644 --- a/examples/swift/hello_world/LoggerCustomer.swift +++ b/examples/swift/hello_world/LoggerCustomer.swift @@ -91,10 +91,11 @@ final class LoggerCustomer: NSObject, URLSessionDelegate { .start( withAPIKey: Configuration.storedAPIKey ?? "", sessionStrategy: .fixed(), + configuration: Capture.Configuration(enableFatalIssueReporting: true), fieldProviders: [CustomFieldProvider()], apiURL: apiURL )? - .enableIntegrations([.urlSession(), .crashReporting()], disableSwizzling: true) + .enableIntegrations([.urlSession()], disableSwizzling: true) Logger.addField(withKey: "field_container_field_key", value: "field_container_value") Logger.logInfo("App launched. Logger configured.") diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt index 5e83a41ff..ace3532bf 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt @@ -106,7 +106,11 @@ object Capture { private val mainThreadHandler by lazy { MainThreadHandler() } /** - * Initializes fatal issue (ANR, JVM Crash, Native crash) reporting mechanism. + * WARNING: For now this API is not exposed to customers. If there is a request for this + * will open visibility again + * + * Initializes fatal issue reporting mechanism that integrates + * with existing configured crash libraries * * @param fatalIssueMechanism the [FatalIssueMechanism] to use for crash detection * @param context an optional context reference. You should provide the context if called from a [android.content.ContentProvider] @@ -116,16 +120,12 @@ object Capture { @Suppress("UnusedPrivateMember") @ExperimentalBitdriftApi @JvmStatic - @JvmOverloads - fun initFatalIssueReporting( - fatalIssueMechanism: FatalIssueMechanism = FatalIssueMechanism.BuiltIn, - context: Context? = null, - ) { + private fun initIntegrationFatalIssueReporting(context: Context? = null) { if (context == null && !ContextHolder.isInitialized) { Log.w(LOG_TAG, "Attempted to initialize Fatal Issue Reporting with a null context. Skipping enabling crash tracking.") return } - fatalIssueReporter.initialize(context?.applicationContext ?: ContextHolder.APP_CONTEXT, fatalIssueMechanism) + fatalIssueReporter.initIntegrationMode(context?.applicationContext ?: ContextHolder.APP_CONTEXT) } /** @@ -141,6 +141,8 @@ object Capture { * @param apiUrl The base URL of Capture API. Depend on its default value unless specifically * instructed otherwise during discussions with bitdrift. Defaults to bitdrift's hosted * Compose API base URL. + * @param context an optional context reference. You should provide the context if called from + * a [android.content.ContentProvider] */ @Synchronized @JvmStatic @@ -152,6 +154,7 @@ object Capture { fieldProviders: List = listOf(), dateProvider: DateProvider? = null, apiUrl: HttpUrl = defaultCaptureApiUrl, + context: Context? = null, ) { start( apiKey, @@ -161,6 +164,7 @@ object Capture { dateProvider, apiUrl, CaptureJniLibrary, + context, ) } @@ -175,17 +179,17 @@ object Capture { dateProvider: DateProvider? = null, apiUrl: HttpUrl = defaultCaptureApiUrl, bridge: IBridge, + context: Context? = null, backgroundThreadHandler: IBackgroundThreadHandler = CaptureDispatchers.CommonBackground, ) { // Note that we need to use @Synchronized to prevent multiple loggers from being initialized, // while subsequent logger access relies on volatile reads. // There's nothing we can do if we don't have yet access to the application context. - if (!ContextHolder.isInitialized) { + if (context == null && !ContextHolder.isInitialized) { Log.w( LOG_TAG, - "Attempted to initialize Capture before androidx.startup.Initializers " + - "are run. Aborting logger initialization.", + "Attempted to initialize Capture with a null context", ) return } @@ -194,10 +198,15 @@ object Capture { if (default.compareAndSet(LoggerState.NotStarted, LoggerState.Starting(preInitInMemoryLogger))) { backgroundThreadHandler.runAsync { try { + val unwrappedContext = context?.applicationContext ?: ContextHolder.APP_CONTEXT + if (configuration.enableFatalIssueReporting) { + fatalIssueReporter.initBuiltInMode(unwrappedContext) + } val loggerImpl = LoggerImpl( apiKey = apiKey, apiUrl = apiUrl, + context = unwrappedContext, fieldProviders = fieldProviders, dateProvider = dateProvider ?: SystemDateProvider(), configuration = configuration, diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Configuration.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Configuration.kt index f71142baa..48a40a214 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Configuration.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Configuration.kt @@ -13,10 +13,13 @@ import io.bitdrift.capture.replay.SessionReplayConfiguration * A configuration object representing the feature set enabled for Capture. * @param sessionReplayConfiguration The resource reporting configuration to use. Passing `null` disables the feature. * @param sleepMode SleepMode.ACTIVE if Capture should initialize in minimal activity mode + * @param enableFatalIssueReporting When set to true wil capture Fatal Issues automatically [JVM crash, ANR, etc] and without requiring + * any external 3rd party library integration */ data class Configuration @JvmOverloads constructor( val sessionReplayConfiguration: SessionReplayConfiguration = SessionReplayConfiguration(), val sleepMode: SleepMode = SleepMode.INACTIVE, + val enableFatalIssueReporting: Boolean = false, ) diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt index ab3eb4cd6..4358d6bcb 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt @@ -71,7 +71,7 @@ internal class LoggerImpl( dateProvider: DateProvider, private val errorHandler: ErrorHandler = ErrorHandler(), sessionStrategy: SessionStrategy, - context: Context = ContextHolder.APP_CONTEXT, + context: Context, clientAttributes: ClientAttributes = ClientAttributes( context, diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/PreInitInMemoryLogger.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/PreInitInMemoryLogger.kt index 8450023b3..a0e14b31f 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/PreInitInMemoryLogger.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/PreInitInMemoryLogger.kt @@ -93,6 +93,10 @@ internal class PreInitInMemoryLogger : return span } + override fun setSleepMode(sleepMode: SleepMode) { + addLoggerCall { it.setSleepMode(sleepMode) } + } + override fun log(httpRequestInfo: HttpRequestInfo) { addLoggerCall { it.log(httpRequestInfo) } } diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/FatalIssueMechanism.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/FatalIssueMechanism.kt index a2bcdcce1..bc8c663a3 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/FatalIssueMechanism.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/FatalIssueMechanism.kt @@ -21,7 +21,7 @@ sealed class FatalIssueMechanism( * Use this option to integrate with an existing fatal issue reporting mechanism. * This will scan for specific fatal issues on the configured directory. */ - data object Integration : FatalIssueMechanism("INTEGRATION") + internal data object Integration : FatalIssueMechanism("INTEGRATION") /** * Built-in fatal issue reporter implementation that doesn't rely on any 3rd party integration diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/FatalIssueReporter.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/FatalIssueReporter.kt index f817c7d83..1203a942d 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/FatalIssueReporter.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/FatalIssueReporter.kt @@ -30,10 +30,8 @@ import io.bitdrift.capture.reports.persistence.FatalIssueReporterStorage import io.bitdrift.capture.reports.processor.FatalIssueReporterProcessor import io.bitdrift.capture.utils.SdkDirectory import java.io.File -import java.lang.Thread import java.nio.file.Files import java.nio.file.attribute.FileTime -import kotlin.time.Duration import kotlin.time.DurationUnit import kotlin.time.measureTime @@ -54,18 +52,49 @@ internal class FatalIssueReporter( private var initializationCallerThread: String = NotInitialized.readableType /** - * Initializes the fatal issue reporting with the specified [io.bitdrift.capture.reports.FatalIssueMechanism] + * Initializes integration fatal issue mechanism (a.k.a vendor) */ - override fun initialize( - appContext: Context, - fatalIssueMechanism: FatalIssueMechanism, - ) { - initializationCallerThread = Thread.currentThread().name - if (fatalIssueReporterStatus.state is FatalIssueReporterState.NotInitialized) { - if (fatalIssueMechanism == FatalIssueMechanism.Integration) { - fatalIssueReporterStatus = setupIntegrationReporting(appContext) - } else if (fatalIssueMechanism == FatalIssueMechanism.BuiltIn) { - fatalIssueReporterStatus = setupBuiltInReporting(appContext) + override fun initIntegrationMode(appContext: Context) { + if (fatalIssueReporterStatus.state is NotInitialized) { + initializationCallerThread = Thread.currentThread().name + fatalIssueReporterStatus = setupIntegrationReporting(appContext) + } else { + Log.e(LOG_TAG, "Fatal issue reporting already being initialized") + } + } + + /** + * Initializes a BuiltIn Fatal Issue reporting mechanism that doesn't depend on any 3rd party + * libraries + */ + override fun initBuiltInMode(appContext: Context) { + if (fatalIssueReporterStatus.state is NotInitialized) { + runCatching { + initializationCallerThread = Thread.currentThread().name + var fatalIssueReporterState: FatalIssueReporterState + val duration = + measureTime { + val destinationDirectory = getFatalIssueDirectories(appContext) + fatalIssueReporterProcessor = + FatalIssueReporterProcessor( + appContext, + FatalIssueReporterStorage(destinationDirectory.destinationDirectory), + ) + captureUncaughtExceptionHandler.install(this) + persistLastExitReasonIfNeeded(appContext) + fatalIssueReporterState = FatalIssueReporterState.BuiltIn.Initialized + } + fatalIssueReporterState to duration + fatalIssueReporterStatus = + FatalIssueReporterStatus( + fatalIssueReporterState, + duration, + FatalIssueMechanism.BuiltIn, + ) + }.getOrElse { + val errorMessage = + "Error while initializing reporter for ${FatalIssueMechanism.BuiltIn}. $it" + Log.e(LOG_TAG, errorMessage) } } else { Log.e(LOG_TAG, "Fatal issue reporting already being initialized") @@ -108,64 +137,35 @@ internal class FatalIssueReporter( .toFieldValue(), ) - private fun performReportingSetup( - mechanism: FatalIssueMechanism, - setupAction: () -> Pair, - cleanup: () -> Unit = {}, - ): FatalIssueReporterStatus = - runCatching { + private fun setupIntegrationReporting(appContext: Context): FatalIssueReporterStatus { + val mechanism = FatalIssueMechanism.Integration + return runCatching { val (fatalIssueReporterState, duration) = - mainThreadHandler.runAndReturnResult { setupAction() } + mainThreadHandler.runAndReturnResult { + var fatalIssueReporterState: FatalIssueReporterState + val duration = + measureTime { + val fatalIssueDirectories = getFatalIssueDirectories(appContext) + fatalIssueReporterState = + verifyDirectoriesAndCopyFiles(appContext, fatalIssueDirectories) + } + fatalIssueReporterState to duration + } FatalIssueReporterStatus( state = fatalIssueReporterState, duration = duration, mechanism = mechanism, ) }.getOrElse { - cleanup() - val errorMessage = "Error while initializing reporter for $mechanism mode. ${it.message}" + val errorMessage = + "Error while initializing reporter for $mechanism mode. ${it.message}" Log.e(LOG_TAG, errorMessage) FatalIssueReporterStatus( FatalIssueReporterState.ProcessingFailure(mechanism, errorMessage), mechanism = mechanism, ) } - - private fun setupIntegrationReporting(appContext: Context): FatalIssueReporterStatus = - performReportingSetup( - FatalIssueMechanism.Integration, - setupAction = { - var fatalIssueReporterState: FatalIssueReporterState - val duration = - measureTime { - val fatalIssueDirectories = getFatalIssueDirectories(appContext) - fatalIssueReporterState = verifyDirectoriesAndCopyFiles(appContext, fatalIssueDirectories) - } - fatalIssueReporterState to duration - }, - ) - - private fun setupBuiltInReporting(appContext: Context): FatalIssueReporterStatus = - performReportingSetup( - FatalIssueMechanism.BuiltIn, - setupAction = { - var fatalIssueReporterState: FatalIssueReporterState - val duration = - measureTime { - val destinationDirectory = getFatalIssueDirectories(appContext) - fatalIssueReporterProcessor = - FatalIssueReporterProcessor( - appContext, - FatalIssueReporterStorage(destinationDirectory.destinationDirectory), - ) - captureUncaughtExceptionHandler.install(this) - persistLastExitReasonIfNeeded(appContext) - fatalIssueReporterState = FatalIssueReporterState.BuiltIn.Initialized - } - fatalIssueReporterState to duration - }, - cleanup = { captureUncaughtExceptionHandler.uninstall() }, - ) + } private fun persistLastExitReasonIfNeeded(appContext: Context) { val activityManager: ActivityManager = diff --git a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/IFatalIssueReporter.kt b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/IFatalIssueReporter.kt index 5c686387d..417903153 100644 --- a/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/IFatalIssueReporter.kt +++ b/platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/reports/IFatalIssueReporter.kt @@ -15,12 +15,14 @@ import io.bitdrift.capture.providers.FieldValue */ interface IFatalIssueReporter { /** - * Initializes the reporter + * Initializes the Integration mode reporter */ - fun initialize( - appContext: Context, - fatalIssueMechanism: FatalIssueMechanism, - ) + fun initIntegrationMode(appContext: Context) + + /** + * Initializes the BuiltIn reporter + */ + fun initBuiltInMode(appContext: Context) /** * Returns the configured [io.bitdrift.capture.reports.FatalIssueMechanism] diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt index 5a20df1bc..85db63449 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerSessionOverrideTest.kt @@ -88,6 +88,7 @@ class CaptureLoggerSessionOverrideTest { dateProvider = systemDateProvider, sessionStrategy = SessionStrategy.Fixed { "foo" }, configuration = Configuration(), + context = ContextHolder.APP_CONTEXT, preferences = preferences, fatalIssueReporter = fatalIssueReporter, preInitLogFlusher = preInitLogFlusher, @@ -121,6 +122,7 @@ class CaptureLoggerSessionOverrideTest { dateProvider = systemDateProvider, sessionStrategy = SessionStrategy.Fixed { "bar" }, configuration = Configuration(), + context = ContextHolder.APP_CONTEXT, preferences = preferences, activityManager = activityManager, fatalIssueReporter = fatalIssueReporter, diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt index aba89210b..2c581e807 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/CaptureLoggerTest.kt @@ -470,6 +470,7 @@ class CaptureLoggerTest { apiUrl = testServerUrl(), fieldProviders = fieldProviders, sessionStrategy = sessionStrategy, + context = ContextHolder.APP_CONTEXT, dateProvider = dateProvider, configuration = Configuration(), fatalIssueReporter = fatalIssueReporter, diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ErrorReporterTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ErrorReporterTest.kt index 6c530e604..a10a1d2df 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ErrorReporterTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/ErrorReporterTest.kt @@ -108,6 +108,7 @@ class ErrorReporterTest { apiUrl = testServerUrl(), fieldProviders = listOf(), dateProvider = SystemDateProvider(), + context = ContextHolder.APP_CONTEXT, sessionStrategy = SessionStrategy.Fixed { "SESSION_ID" }, configuration = Configuration(), errorReporter = reporter, diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterTest.kt index e79e7c1db..006322188 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/FatalIssueReporterTest.kt @@ -10,17 +10,13 @@ package io.bitdrift.capture import android.content.Context import androidx.test.core.app.ApplicationProvider import com.nhaarman.mockitokotlin2.any -import com.nhaarman.mockitokotlin2.doAnswer import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify import io.bitdrift.capture.ContextHolder.Companion.APP_CONTEXT import io.bitdrift.capture.common.MainThreadHandler -import io.bitdrift.capture.fakes.FakeJvmException import io.bitdrift.capture.providers.FieldValue import io.bitdrift.capture.providers.toFieldValue -import io.bitdrift.capture.reports.FatalIssueMechanism import io.bitdrift.capture.reports.FatalIssueReporter import io.bitdrift.capture.reports.FatalIssueReporter.Companion.buildFieldsMap import io.bitdrift.capture.reports.FatalIssueReporter.Companion.getDuration @@ -57,10 +53,7 @@ class FatalIssueReporterTest { fun initialize_whenIntegrationMechanismAndMissingConfigFile_shouldReportMissingConfigState() { prepareFileDirectories(doesReportsDirectoryExist = false) - fatalIssueReporter.initialize( - appContext, - fatalIssueMechanism = FatalIssueMechanism.Integration, - ) + fatalIssueReporter.initIntegrationMode(appContext) fatalIssueReporter .fatalIssueReporterStatus @@ -76,10 +69,7 @@ class FatalIssueReporterTest { crashFilePresent = false, ) - fatalIssueReporter.initialize( - appContext, - fatalIssueMechanism = FatalIssueMechanism.Integration, - ) + fatalIssueReporter.initIntegrationMode(appContext) fatalIssueReporter.fatalIssueReporterStatus.assert( FatalIssueReporterState.Integration.WithoutPriorFatalIssue::class.java, @@ -95,10 +85,7 @@ class FatalIssueReporterTest { crashFilePresent = false, ) - fatalIssueReporter.initialize( - appContext, - fatalIssueMechanism = FatalIssueMechanism.Integration, - ) + fatalIssueReporter.initIntegrationMode(appContext) fatalIssueReporter.fatalIssueReporterStatus.assert( FatalIssueReporterState.Integration.InvalidConfigDirectory::class.java, @@ -123,10 +110,7 @@ class FatalIssueReporterTest { crashFilePresent = true, ) - fatalIssueReporter.initialize( - appContext, - fatalIssueMechanism = FatalIssueMechanism.Integration, - ) + fatalIssueReporter.initIntegrationMode(appContext) fatalIssueReporter.fatalIssueReporterStatus.assert( FatalIssueReporterState.Integration.WithoutPriorFatalIssue::class.java, @@ -141,10 +125,7 @@ class FatalIssueReporterTest { crashFilePresent = true, ) - fatalIssueReporter.initialize( - appContext, - fatalIssueMechanism = FatalIssueMechanism.Integration, - ) + fatalIssueReporter.initIntegrationMode(appContext) fatalIssueReporter.fatalIssueReporterStatus.assert( FatalIssueReporterState.Integration.MalformedConfigFile::class.java, @@ -153,7 +134,7 @@ class FatalIssueReporterTest { @Test fun initialize_whenBuiltInMechanism_shouldInitCrashHandlerAndFetchAppExitReason() { - fatalIssueReporter.initialize(appContext, fatalIssueMechanism = FatalIssueMechanism.BuiltIn) + fatalIssueReporter.initBuiltInMode(appContext) verify(captureUncaughtExceptionHandler).install(eq(fatalIssueReporter)) verify(latestAppExitInfoProvider).get(any()) @@ -162,30 +143,6 @@ class FatalIssueReporterTest { ) } - @Test - fun initialize_whenBuiltInMechanismAndArtificialError_shouldReportFailedInitState() { - val mainThreadHandlerWithException: MainThreadHandler = - mock { - on { run(any()) } doAnswer { throw FakeJvmException() } - on { runAndReturnResult(any()) } doAnswer { throw FakeJvmException() } - } - val fatalIssueReporter = buildReporter(mainThreadHandlerWithException) - - fatalIssueReporter.initialize(appContext, FatalIssueMechanism.BuiltIn) - - verify(captureUncaughtExceptionHandler, never()).install(any()) - verify(captureUncaughtExceptionHandler).uninstall() - verify(latestAppExitInfoProvider, never()).get(any()) - fatalIssueReporter.fatalIssueReporterStatus.assert( - FatalIssueReporterState.ProcessingFailure::class.java, - ) - val state = - fatalIssueReporter.fatalIssueReporterStatus.state as FatalIssueReporterState.ProcessingFailure - assertThat(state.fatalIssueMechanism).isEqualTo(FatalIssueMechanism.BuiltIn) - assertThat(state.errorMessage) - .isEqualTo("Error while initializing reporter for BuiltIn mode. Fake JVM exception") - } - private fun FatalIssueReporterStatus.assert( expectedType: Class<*>, crashFileExist: Boolean = false, @@ -269,10 +226,7 @@ class FatalIssueReporterTest { crashFilePresent = true, ) - fatalIssueReporter.initialize( - appContext, - fatalIssueMechanism = FatalIssueMechanism.Integration, - ) + fatalIssueReporter.initIntegrationMode(appContext) fatalIssueReporter.fatalIssueReporterStatus.assert( FatalIssueReporterState.Integration.FatalIssueReportSent::class.java, diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/PreInitInMemoryLoggerTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/PreInitInMemoryLoggerTest.kt index 049d678d4..375efff63 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/PreInitInMemoryLoggerTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/PreInitInMemoryLoggerTest.kt @@ -97,6 +97,8 @@ class PreInitInMemoryLoggerTest { parentSpanId: UUID?, ): Span = Span(null, name, level, fields, startTimeMs, parentSpanId) + override fun setSleepMode(sleepMode: SleepMode) {} + override fun log(httpRequestInfo: HttpRequestInfo) {} override fun log(httpResponseInfo: HttpResponseInfo) {} diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt index 7287afb07..87593376c 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionStrategyTest.kt @@ -45,6 +45,7 @@ class SessionStrategyTest { apiUrl = testServerUrl(), fieldProviders = listOf(), dateProvider = mock(), + context = ContextHolder.APP_CONTEXT, sessionStrategy = SessionStrategy.Fixed { val sessionId = UUID.randomUUID().toString() @@ -80,6 +81,7 @@ class SessionStrategyTest { apiUrl = testServerUrl(), fieldProviders = listOf(), dateProvider = mock(), + context = ContextHolder.APP_CONTEXT, sessionStrategy = SessionStrategy.ActivityBased { observedSessionId = it diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionUrlTest.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionUrlTest.kt index 502c39423..ae9aa9a08 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionUrlTest.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/SessionUrlTest.kt @@ -75,6 +75,7 @@ class SessionUrlTest { apiUrl = apiUrl.toHttpUrl(), configuration = Configuration(), fieldProviders = listOf(), + context = ContextHolder.APP_CONTEXT, dateProvider = SystemDateProvider(), sessionStrategy = SessionStrategy.Fixed { "SESSION_ID" }, fatalIssueReporter = fatalIssueReporter, diff --git a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/fakes/FakeFatalIssueReporter.kt b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/fakes/FakeFatalIssueReporter.kt index e8b635bba..4c75481e7 100644 --- a/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/fakes/FakeFatalIssueReporter.kt +++ b/platform/jvm/capture/src/test/kotlin/io/bitdrift/capture/fakes/FakeFatalIssueReporter.kt @@ -17,10 +17,11 @@ import io.bitdrift.capture.reports.IFatalIssueReporter class FakeFatalIssueReporter : IFatalIssueReporter { private var fatalIssueMechanism: FatalIssueMechanism = FatalIssueMechanism.BuiltIn - override fun initialize( - appContext: Context, - fatalIssueMechanism: FatalIssueMechanism, - ) { + override fun initIntegrationMode(appContext: Context) { + // no-op + } + + override fun initBuiltInMode(appContext: Context) { // no-op } diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/BitdriftInit.java b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/BitdriftInit.java index e0c5b6ce3..c432830ab 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/BitdriftInit.java +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/BitdriftInit.java @@ -23,7 +23,8 @@ public class BitdriftInit { public static void initBitdriftCaptureInJava( HttpUrl apiUrl, String apiKey, - String sessionStrategyName) { + String sessionStrategyName, + Configuration configuration) { String userID = UUID.randomUUID().toString(); List fieldProviders = new ArrayList<>(); fieldProviders.add(() -> { @@ -35,7 +36,7 @@ public static void initBitdriftCaptureInJava( Capture.Logger.start( apiKey, mapToSessionStrategy(sessionStrategyName), - new Configuration(), + configuration, fieldProviders, null, apiUrl diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ConfigurationSettingsFragment.kt b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ConfigurationSettingsFragment.kt index e498adeb1..6775970c3 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ConfigurationSettingsFragment.kt +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/ConfigurationSettingsFragment.kt @@ -72,7 +72,7 @@ class ConfigurationSettingsFragment : PreferenceFragmentCompat() { FATAL_ISSUE_SOURCE_PREFS_KEY, FATAL_ISSUE_TYPE_TITLE, FATAL_ISSUE_REPORTING_TYPES, - FatalIssueMechanism.Integration.displayName, + FatalIssueMechanism.BuiltIn.displayName, context ) backendCategory.addPreference(sessionStrategyPref) @@ -115,22 +115,17 @@ class ConfigurationSettingsFragment : PreferenceFragmentCompat() { const val FATAL_ISSUE_SOURCE_PREFS_KEY = "fatalIssueSource" private const val SELECTION_SUMMARY = "App needs to be restarted for changes to take effect" private const val SESSION_STRATEGY_TITLE = "Session Strategy" - private const val FATAL_ISSUE_TYPE_TITLE = "Fatal Issue Reporting Source Type" + private const val FATAL_ISSUE_TYPE_TITLE = "Fatal Issue Mechanism" private val FATAL_ISSUE_REPORTING_TYPES = arrayOf( FatalIssueMechanism.BuiltIn.displayName, - FatalIssueMechanism.Integration.displayName) + "NONE") private val SESSION_STRATEGY_ENTRIES = arrayOf(SessionStrategyPreferences.FIXED.displayName, SessionStrategyPreferences.ACTIVITY_BASED.displayName) - fun getFatalIssueSourceConfig(sharedPreferences: SharedPreferences):FatalIssueMechanism{ - val displayName = sharedPreferences.getString(FATAL_ISSUE_SOURCE_PREFS_KEY, null) - return if(displayName == FatalIssueMechanism.BuiltIn.displayName){ - FatalIssueMechanism.BuiltIn - }else { - FatalIssueMechanism.Integration - } + fun getFatalIssueSourceConfig(sharedPreferences: SharedPreferences):String{ + return sharedPreferences.getString(FATAL_ISSUE_SOURCE_PREFS_KEY, null)?:"" } } } diff --git a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt index e65254e95..5ffdd7958 100644 --- a/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt +++ b/platform/jvm/gradle-test-app/src/main/java/io/bitdrift/gradletestapp/GradleTestApp.kt @@ -55,10 +55,12 @@ import com.bugsnag.android.Bugsnag import com.bugsnag.android.Logger import io.bitdrift.capture.Capture import io.bitdrift.capture.Capture.Logger.sessionUrl +import io.bitdrift.capture.Configuration import io.bitdrift.capture.LogLevel import io.bitdrift.capture.events.span.Span import io.bitdrift.capture.events.span.SpanResult import io.bitdrift.capture.experimental.ExperimentalBitdriftApi +import io.bitdrift.capture.reports.FatalIssueMechanism import io.bitdrift.capture.timber.CaptureTree import io.bitdrift.gradletestapp.ConfigurationSettingsFragment.Companion.SESSION_STRATEGY_PREFS_KEY import io.bitdrift.gradletestapp.ConfigurationSettingsFragment.Companion.getFatalIssueSourceConfig @@ -98,9 +100,7 @@ class GradleTestApp : Application() { private fun initLogging() { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) - @OptIn(ExperimentalBitdriftApi::class) - Capture.Logger.initFatalIssueReporting(fatalIssueMechanism = getFatalIssueSourceConfig(sharedPreferences)) - + val fatalIssueMechanism = getFatalIssueSourceConfig(sharedPreferences) val stringApiUrl = sharedPreferences.getString("apiUrl", null) val apiUrl = stringApiUrl?.toHttpUrlOrNull() if (apiUrl == null) { @@ -108,10 +108,13 @@ class GradleTestApp : Application() { return } + val enableFatalIssueReporting = fatalIssueMechanism == FatalIssueMechanism.BuiltIn.displayName + val configuration = Configuration(enableFatalIssueReporting = enableFatalIssueReporting) BitdriftInit.initBitdriftCaptureInJava( apiUrl, sharedPreferences.getString(BITDRIFT_API_KEY, ""), sharedPreferences.getString(SESSION_STRATEGY_PREFS_KEY, FIXED.displayName), + configuration, ) // Timber if (BuildConfig.DEBUG) { diff --git a/platform/swift/source/Capture.swift b/platform/swift/source/Capture.swift index 0d58e2ad8..ec4c535ef 100644 --- a/platform/swift/source/Capture.swift +++ b/platform/swift/source/Capture.swift @@ -81,6 +81,9 @@ extension Logger { ) -> LoggerIntegrator? { return self.createOnce { + if configuration.enableFatalIssueReporting { + Capture.Logger.initFatalIssueReporting(.builtIn) + } return Logger( withAPIKey: apiKey, apiURL: apiURL, @@ -96,8 +99,10 @@ extension Logger { /// Initializes the issue reporting mechanism. Must be called prior to `Logger.start()` /// This API is experimental and subject to change /// + /// WARNING: For now this API is not exposed to customers. If there is a request for this will open visibility again + /// /// - parameter type: mechanism for crash detection - public static func initFatalIssueReporting(_ type: IssueReporterType = .builtIn) { + static func initFatalIssueReporting(_ type: IssueReporterType = .builtIn) { if issueReporterInitResult.0 != .notInitialized { log(level: .warning, message: "Fatal issue reporting already being initialized") return diff --git a/platform/swift/source/LoggerObjc.swift b/platform/swift/source/LoggerObjc.swift index 22de85c9f..41029fba8 100644 --- a/platform/swift/source/LoggerObjc.swift +++ b/platform/swift/source/LoggerObjc.swift @@ -67,6 +67,7 @@ public final class LoggerObjc: NSObject { /// Defaults to Bitdrift's hosted Compose API base URL. /// - parameter enableURLSessionIntegration: A flag indicating if automatic URLSession capture is enabled. /// - parameter sleepMode: .active if Capture should be initialized in minimal activity mode. + /// - parameter enableFatalIssueReporting: true if Capture should enable Fatal Issue Reporting. @objc public static func start( withAPIKey apiKey: String, @@ -74,13 +75,14 @@ public final class LoggerObjc: NSObject { // swiftlint:disable:next force_unwrapping use_static_string_url_init apiURL: URL = URL(string: "https://api.bitdrift.io")!, enableURLSessionIntegration: Bool = true, - sleepMode: SleepMode = .inactive + sleepMode: SleepMode = .inactive, + enableFatalIssueReporting: Bool = false ) { let logger = Capture.Logger .start( withAPIKey: apiKey, sessionStrategy: sessionStrategy.underlyingSessionStrategy, - configuration: Configuration(sleepMode: sleepMode), + configuration: Configuration(sleepMode: sleepMode, enableFatalIssueReporting: enableFatalIssueReporting), apiURL: apiURL ) @@ -91,17 +93,20 @@ public final class LoggerObjc: NSObject { /// Initializes the issue reporting mechanism. Must be called prior to `Logger.start()` /// This API is experimental and subject to change + /// WARNING: For now this API is not exposed to customers. If there is a request for this will open visibility again @objc - public static func initFatalIssueReporting() { + static func initFatalIssueReporting() { Capture.Logger.initFatalIssueReporting(.builtIn) } /// Initializes the issue reporting mechanism. Must be called prior to `Logger.start()` /// This API is experimental and subject to change /// + /// WARNING: For now this API is not exposed to customers. If there is a request for this will open visibility again + /// /// - parameter type: mechanism for crash detection @objc - public static func initFatalIssueReporting(withType type: IssueReporterTypeObjc) { + static func initFatalIssueReporting(withType type: IssueReporterTypeObjc) { switch type { case .builtIn: Capture.Logger.initFatalIssueReporting(.builtIn) diff --git a/platform/swift/source/features/Configuration.swift b/platform/swift/source/features/Configuration.swift index cd56d662d..18ab834e2 100644 --- a/platform/swift/source/features/Configuration.swift +++ b/platform/swift/source/features/Configuration.swift @@ -15,15 +15,21 @@ public struct Configuration { /// .active if Capture should initialize in minimal activity mode public var sleepMode: SleepMode + /// true if Capture should enable Fatal Issue Reporting + public var enableFatalIssueReporting: Bool + /// Initializes a new instance of the Capture configuration. /// /// - parameter sessionReplayConfiguration: The session replay configuration to use. /// - parameter sleepMode: .active if Capture should initialize in minimal activity mode + /// - parameter enableFatalIssueReporting: true if Capture should enable Fatal Issue Reporting public init( sessionReplayConfiguration: SessionReplayConfiguration = .init(), - sleepMode: SleepMode = .inactive + sleepMode: SleepMode = .inactive, + enableFatalIssueReporting: Bool = false ) { self.sessionReplayConfiguration = sessionReplayConfiguration self.sleepMode = sleepMode + self.enableFatalIssueReporting = enableFatalIssueReporting } } diff --git a/platform/swift/source/reports/CrashReportingIntegration.swift b/platform/swift/source/reports/CrashReportingIntegration.swift deleted file mode 100644 index cad62d283..000000000 --- a/platform/swift/source/reports/CrashReportingIntegration.swift +++ /dev/null @@ -1,19 +0,0 @@ -// 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 - -extension Integration { - /// Enables reporting unexpected app terminations - /// - /// - parameter type: mechanism used for crash detection - /// - /// - returns: The crash reporting integration - public static func crashReporting(_ type: IssueReporterType = .builtIn) -> Integration { - .init { _, _ in - Logger.initFatalIssueReporting(type) - } - } -}