From 25d717babf57af31600cecace6004d3fee20e5ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kosmaty?= Date: Fri, 4 Feb 2022 18:40:34 +0100 Subject: [PATCH] [dev-launcher][Android] Send uncaught exceptions to bundler server (#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. --- .../devlauncher/DevLauncherController.kt | 21 ++++--- ...sion.kt => DevLauncherOkHttpExtensions.kt} | 16 +++++- .../DevLauncherUncaughtExceptionHandler.kt | 43 +++++++++++++- .../devlauncher/logs/DevLauncherRemoteLog.kt | 56 +++++++++++++++++++ .../logs/DevLauncherRemoteLogManager.kt | 49 ++++++++++++++++ 5 files changed, 174 insertions(+), 11 deletions(-) rename packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/helpers/{DevLauncherOkHttpExtension.kt => DevLauncherOkHttpExtensions.kt} (72%) create mode 100644 packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/logs/DevLauncherRemoteLog.kt create mode 100644 packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/logs/DevLauncherRemoteLogManager.kt diff --git a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt index 69f0b3c3bc169..0a3567e352280 100644 --- a/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt +++ b/packages/expo-dev-launcher/android/src/debug/java/expo/modules/devlauncher/DevLauncherController.kt @@ -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(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(DevLauncherController()) } @JvmStatic diff --git a/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/helpers/DevLauncherOkHttpExtension.kt b/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/helpers/DevLauncherOkHttpExtensions.kt similarity index 72% rename from packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/helpers/DevLauncherOkHttpExtension.kt rename to packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/helpers/DevLauncherOkHttpExtensions.kt index a7e7d3d248962..7b79ebdf0cac5 100644 --- a/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/helpers/DevLauncherOkHttpExtension.kt +++ b/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/helpers/DevLauncherOkHttpExtensions.kt @@ -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 @@ -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): Request = + Request + .Builder() + .method("POST", requestBody) + .url(url.toString()) + .apply { + headers.forEach { + addHeader(it.first, it.second) + + } + } + .build() diff --git a/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/launcher/errors/DevLauncherUncaughtExceptionHandler.kt b/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/launcher/errors/DevLauncherUncaughtExceptionHandler.kt index 0c776ed93d74f..c27fdb2ed997b 100644 --- a/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/launcher/errors/DevLauncherUncaughtExceptionHandler.kt +++ b/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/launcher/errors/DevLauncherUncaughtExceptionHandler.kt @@ -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 { @@ -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( @@ -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. @@ -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() + } } diff --git a/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/logs/DevLauncherRemoteLog.kt b/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/logs/DevLauncherRemoteLog.kt new file mode 100644 index 0000000000000..44a2b354b8cff --- /dev/null +++ b/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/logs/DevLauncherRemoteLog.kt @@ -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 +} diff --git a/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/logs/DevLauncherRemoteLogManager.kt b/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/logs/DevLauncherRemoteLogManager.kt new file mode 100644 index 0000000000000..4213aee72c156 --- /dev/null +++ b/packages/expo-dev-launcher/android/src/main/java/expo/modules/devlauncher/logs/DevLauncherRemoteLogManager.kt @@ -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 = 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() + } +}