Skip to content

Commit

Permalink
[dev-launcher][Android] Send uncaught exceptions to bundler server (#…
Browse files Browse the repository at this point in the history
…15938)

# Why

Part of ENG-2401.

# How

Send uncaught exceptions to the bundler server if possible. Our dev server has its endpoint where we can send logs. So I've just used it. 

# Test Plan

- throws an exception in the start method of the app and check if a log occurs in the console.
  • Loading branch information
lukmccall committed Feb 4, 2022
1 parent cbcd271 commit 25d717b
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -340,23 +340,26 @@ class DevLauncherController private constructor()
if (!testInterceptor.allowReinitialization()) {
check(!wasInitialized()) { "DevelopmentClientController was initialized." }
}
MenuDelegateWasInitialized = false
DevLauncherKoinContext.app.koin.loadModules(listOf(
module {
single { context }
single { appHost }
}
), allowOverride = true)

val controller = DevLauncherController()
DevLauncherKoinContext.app.koin.declare<DevLauncherControllerInterface>(controller)

if (!sErrorHandlerWasInitialized && context is Application) {
val handler = DevLauncherUncaughtExceptionHandler(
controller,
context,
Thread.getDefaultUncaughtExceptionHandler()
)
Thread.setDefaultUncaughtExceptionHandler(handler)
sErrorHandlerWasInitialized = true
}

MenuDelegateWasInitialized = false
DevLauncherKoinContext.app.koin.loadModules(listOf(
module {
single { context }
single { appHost }
}
), allowOverride = true)
DevLauncherKoinContext.app.koin.declare<DevLauncherControllerInterface>(DevLauncherController())
}

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import okhttp3.Callback
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import java.io.IOException
import kotlin.coroutines.resume
Expand All @@ -32,5 +33,18 @@ suspend inline fun Request.await(okHttpClient: OkHttpClient): Response {
}
}

fun fetch(url: Uri, method: String, headers: Headers) =
fun fetch(url: Uri, method: String, headers: Headers): Request =
Request.Builder().method(method, null).url(url.toString()).headers(headers).build()

fun post(url: Uri, requestBody: RequestBody, vararg headers: Pair<String, String>): Request =
Request
.Builder()
.method("POST", requestBody)
.url(url.toString())
.apply {
headers.forEach {
addHeader(it.first, it.second)

}
}
.build()
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ package expo.modules.devlauncher.launcher.errors
import android.app.Activity
import android.app.Application
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.os.Process
import android.util.Log
import expo.modules.devlauncher.DevLauncherController
import expo.modules.devlauncher.koin.DevLauncherKoinContext
import expo.modules.devlauncher.logs.DevLauncherRemoteLogManager
import java.lang.ref.WeakReference
import java.util.*
import kotlin.concurrent.schedule
import kotlin.system.exitProcess

class DevLauncherUncaughtExceptionHandler(
private val controller: DevLauncherController,
application: Application,
private val defaultUncaughtHandler: Thread.UncaughtExceptionHandler?
) : Thread.UncaughtExceptionHandler {
Expand Down Expand Up @@ -56,6 +60,7 @@ class DevLauncherUncaughtExceptionHandler(
exceptionWasReported = true
Log.e("DevLauncher", "DevLauncher tries to handle uncaught exception.", exception)
tryToSaveException(exception)
tryToSendExceptionToBundler(exception)

applicationHolder.get()?.let {
DevLauncherErrorActivity.showFatalError(
Expand All @@ -66,7 +71,7 @@ class DevLauncherUncaughtExceptionHandler(

// We don't know if the error screen will show up.
// For instance, if the exception was thrown in `MainApplication.onCreate` method,
// the erorr screen won't show up.
// the error screen won't show up.
// That's why we schedule a simple function which will check
// if the error was handle properly or will fallback
// to the default exception handler.
Expand Down Expand Up @@ -94,4 +99,40 @@ class DevLauncherUncaughtExceptionHandler(
val errorRegistry = DevLauncherErrorRegistry(context)
errorRegistry.storeException(exception)
}

private fun tryToSendExceptionToBundler(exception: Throwable) {
if (
controller.mode != DevLauncherController.Mode.APP ||
!controller.appHost.hasInstance() ||
controller.appHost.reactInstanceManager.currentReactContext === null
) {
return
}

try {
val url = getLogsUrl()
val remoteLogManager = DevLauncherRemoteLogManager(DevLauncherKoinContext.app.koin.get(), url)
.apply {
deferError("Your app just crashed. See the error below.")
deferError(exception)
}
remoteLogManager.sendSync()
} catch (e: Throwable) {
Log.e("DevLauncher", "Couldn't send an exception to bundler. $e", e)
}
}

private fun getLogsUrl(): Uri {
val logsUrlFromManifest = controller.manifest?.getRawJson()?.optString("logUrl")
if (logsUrlFromManifest.isNullOrEmpty()) {
return Uri.parse(logsUrlFromManifest)
}

return Uri
.parse(controller.appHost.reactInstanceManager.devSupportManager.sourceUrl)
.buildUpon()
.path("logs")
.clearQuery()
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package expo.modules.devlauncher.logs

import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.annotations.Expose

internal interface DevLauncherRemoteLogBody {
val message: String
val stack: String?

override fun toString(): String
}

internal class DevLauncherSimpleRemoteLogBody(override val message: String) : DevLauncherRemoteLogBody {
override val stack: String? = null

override fun toString(): String = message
}

internal class DevLauncherExceptionRemoteLogBody(exception: Throwable) : DevLauncherRemoteLogBody {
override val message: String = exception.toString()
override val stack: String = exception.stackTraceToRemoteLogString()

override fun toString(): String = Gson().toJson(this)
}

@Suppress("UNUSED")
internal data class DevLauncherRemoteLog(
val logBody: DevLauncherRemoteLogBody,
@Expose val level: String = "error"
) {
@Expose
val includesStack = logBody.stack !== null

@Expose
private val body = logBody.toString()

fun toJson(): String {
return GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.create()
.toJson(this)
}
}

internal fun Throwable.stackTraceToRemoteLogString(): String {
val baseTrace = stackTrace.joinToString(separator = "\n") {
it.toString()
}

cause?.let {
return baseTrace + "\nCaused By ${it.stackTraceToRemoteLogString()}"
}

return baseTrace
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package expo.modules.devlauncher.logs

import android.net.Uri
import android.os.Build
import expo.modules.devlauncher.helpers.await
import expo.modules.devlauncher.helpers.post
import kotlinx.coroutines.runBlocking
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody

class DevLauncherRemoteLogManager(private val httpClient: OkHttpClient, private val url: Uri) {
private val batch: MutableList<DevLauncherRemoteLog> = mutableListOf()

fun deferError(throwable: Throwable) {
addToBatch(
DevLauncherRemoteLog(
DevLauncherExceptionRemoteLogBody(throwable)
)
)
}

fun deferError(message: String) {
addToBatch(
DevLauncherRemoteLog(
DevLauncherSimpleRemoteLogBody(message)
)
)
}

private fun addToBatch(log: DevLauncherRemoteLog) {
batch.add(log)
}

fun sendSync() = runBlocking {
val content = batch.joinToString(separator = ",") { it.toJson() }
val requestBody = RequestBody.create(MediaType.get("application/json"), "[$content]")

val postRequest = post(
url,
requestBody,
"Device-Id" to Build.ID,
"Device-Name" to Build.DISPLAY
)
postRequest.await(httpClient)

batch.clear()
}
}

0 comments on commit 25d717b

Please sign in to comment.