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
7 changes: 2 additions & 5 deletions examples/android/HelloWorldApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<YOUR API KEY GOES HERE>"
private val BITDRIFT_URL = HttpUrl.Builder().scheme("https").host("api.bitdrift.io").build()
Expand All @@ -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();
Expand All @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion examples/swift/hello_world/LoggerCustomer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
29 changes: 19 additions & 10 deletions platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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)
}

/**
Expand All @@ -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
Expand All @@ -152,6 +154,7 @@ object Capture {
fieldProviders: List<FieldProvider> = listOf(),
dateProvider: DateProvider? = null,
apiUrl: HttpUrl = defaultCaptureApiUrl,
context: Context? = null,
) {
start(
apiKey,
Expand All @@ -161,6 +164,7 @@ object Capture {
dateProvider,
apiUrl,
CaptureJniLibrary,
context,
)
}

Expand All @@ -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
}
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

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

no need to include uninstall anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

still needed, sorry it's a bit noisy this was part of the setup action before but now done here directly as part of the initial setup

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")
Expand Down Expand Up @@ -108,64 +137,35 @@ internal class FatalIssueReporter(
.toFieldValue(),
)

private fun performReportingSetup(
mechanism: FatalIssueMechanism,
setupAction: () -> Pair<FatalIssueReporterState, Duration?>,
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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class CaptureLoggerSessionOverrideTest {
dateProvider = systemDateProvider,
sessionStrategy = SessionStrategy.Fixed { "foo" },
configuration = Configuration(),
context = ContextHolder.APP_CONTEXT,
preferences = preferences,
fatalIssueReporter = fatalIssueReporter,
preInitLogFlusher = preInitLogFlusher,
Expand Down Expand Up @@ -121,6 +122,7 @@ class CaptureLoggerSessionOverrideTest {
dateProvider = systemDateProvider,
sessionStrategy = SessionStrategy.Fixed { "bar" },
configuration = Configuration(),
context = ContextHolder.APP_CONTEXT,
preferences = preferences,
activityManager = activityManager,
fatalIssueReporter = fatalIssueReporter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ class CaptureLoggerTest {
apiUrl = testServerUrl(),
fieldProviders = fieldProviders,
sessionStrategy = sessionStrategy,
context = ContextHolder.APP_CONTEXT,
dateProvider = dateProvider,
configuration = Configuration(),
fatalIssueReporter = fatalIssueReporter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading