-
Notifications
You must be signed in to change notification settings - Fork 31
feat: add crash log persistence and its ui tab #271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
b2c7b60
feat: persist crash logs to disk so they survive app restart (#231)
claude 1d57511
refactor: drop unused Config.persistCrashLogs/maxCrashLogLines knobs
claude 6f837a7
refactor: extract File.isDirectChildOf() shared by both storage classes
claude 909382d
feat: surface network error in crash export summary lines
claude ccb707f
perf: fetch crash-handler AppInfo off the app-startup critical path
claude 1c6d0da
simplify: drop the unreachable no-previous-handler fallback in CrashH…
claude 16add04
simplify: inline delegateToPreviousHandler into uncaughtException
claude 0dca7c8
perf: defer crash-record eviction off the synchronous crash-write path
claude 9476851
fix: refresh cached crash-records list after deleteCrashRecord()
claude d878210
cr fixes
Manabu-GT e9070e7
another tweaks
Manabu-GT 05a5a71
cr fixes
Manabu-GT File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
debugoverlay-core/src/main/kotlin/com/ms/square/debugoverlay/internal/crash/CrashHandler.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.ms.square.debugoverlay.internal.crash | ||
|
|
||
| import com.ms.square.debugoverlay.internal.Logger | ||
|
|
||
| /** | ||
| * Captures a crash record on an uncaught exception, then always delegates to | ||
| * [previousHandler] so the app's normal crash behavior (and any other installed crash | ||
| * reporter, e.g. Crashlytics) is unaffected. | ||
| * | ||
| * Installed once by [com.ms.square.debugoverlay.DebugOverlay.install]. [captureCrash] must | ||
| * be non-suspending and should only read in-memory data to complete quickly before the process dies. | ||
| * | ||
| * @param previousHandler The handler that was installed before this one, captured once | ||
| * at install time. Never re-fetched, so a handler installed by another SDK *after* | ||
| * DebugOverlay is never clobbered. | ||
| * @param captureCrash Builds and persists the crash record for the given thread/throwable. | ||
| */ | ||
| internal class CrashHandler( | ||
| private val previousHandler: Thread.UncaughtExceptionHandler?, | ||
| private val captureCrash: (Thread, Throwable) -> Unit, | ||
| ) : Thread.UncaughtExceptionHandler { | ||
|
|
||
| @Suppress("TooGenericExceptionCaught") | ||
| override fun uncaughtException(thread: Thread, throwable: Throwable) { | ||
| try { | ||
| captureCrash(thread, throwable) | ||
| } catch (t: Throwable) { | ||
| // Deliberately broad: capture failure must never prevent the delegate call below | ||
| // from running, since that's what keeps other crash reporters (e.g. Crashlytics) | ||
| // and the platform's own crash handling working. | ||
| runCatching { Logger.e("CrashHandler failed to capture crash record", t) } | ||
| } finally { | ||
| // previousHandler is effectively always non-null on real devices — the platform | ||
| // installs its own default handler before any app code runs, well before | ||
| // DebugOverlay.install() (see class doc). If it's ever null, the crash record above | ||
| // is already persisted; there's nothing more this library needs to do. | ||
| previousHandler?.uncaughtException(thread, throwable) | ||
|
Manabu-GT marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
83 changes: 83 additions & 0 deletions
83
debugoverlay-core/src/main/kotlin/com/ms/square/debugoverlay/internal/crash/CrashRecord.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package com.ms.square.debugoverlay.internal.crash | ||
|
|
||
| import com.ms.square.debugoverlay.internal.bugreport.model.AppInfo | ||
| import com.ms.square.debugoverlay.internal.bugreport.model.CustomLogSourceData | ||
| import com.ms.square.debugoverlay.model.LogEntry | ||
| import com.ms.square.debugoverlay.model.NetworkRequest | ||
| import kotlinx.serialization.EncodeDefault | ||
| import kotlinx.serialization.ExperimentalSerializationApi | ||
| import kotlinx.serialization.Serializable | ||
| import java.io.File | ||
| import java.util.UUID | ||
|
|
||
| /** | ||
| * A crash captured by [CrashHandler] and persisted to disk so it survives process death. | ||
| * | ||
| * @param version Schema version, for forward-compatible reads via `ignoreUnknownKeys`. Carries | ||
| * [EncodeDefault] because it always equals its default, which the serializer would otherwise | ||
| * omit — leaving stored records with no version to read. | ||
| * @param id unique identifier for the crash record | ||
| * @param timestampMs When the exception was caught (epoch millis). | ||
| * @param threadName Name of the thread that crashed. | ||
| * @param exceptionType Fully-qualified exception class name. | ||
| * @param message The exception's message, if any. | ||
| * @param stackTrace Full stack trace text, including any "Caused by" chain. | ||
| * @param appInfo App info captured. | ||
| * @param logcatLogs Recent logcat entries leading up to the crash. | ||
| * @param customLogSourceData Recent custom log source entries, null if none registered. | ||
| * @param networkRequests Recent network requests leading up to the crash. | ||
| */ | ||
| @OptIn(ExperimentalSerializationApi::class) | ||
| @Serializable | ||
| internal data class CrashRecord( | ||
| @EncodeDefault | ||
| val version: Int = 1, | ||
| @EncodeDefault | ||
| val id: String = UUID.randomUUID().toString(), | ||
|
Manabu-GT marked this conversation as resolved.
|
||
| val timestampMs: Long, | ||
| val threadName: String, | ||
| val exceptionType: String, | ||
| val message: String?, | ||
| val stackTrace: String, | ||
| val appInfo: AppInfo?, | ||
| val logcatLogs: List<LogEntry>, | ||
| val customLogSourceData: CustomLogSourceData?, | ||
| val networkRequests: List<NetworkRequestSummary>, | ||
| ) | ||
|
|
||
| /** | ||
| * The subset of a [NetworkRequest] a crash record keeps. | ||
| * | ||
| * Deliberately not [NetworkRequest] itself: that carries request/response bodies (up to 2MB | ||
| * each by default in the OkHttp extension) and [com.ms.square.debugoverlay.model.NetworkError] | ||
| * repeats the response body in its `stackTrace`. Persisting those could mean serializing 2+ | ||
| * megabytes on the crashing thread — most likely to fail exactly when the crash is an | ||
| * OutOfMemoryError — to store data neither the detail screen nor the text export ever renders. | ||
| * These are the only fields both consumers read. | ||
| */ | ||
| @Serializable | ||
| internal data class NetworkRequestSummary( | ||
| val timestampMs: Long, | ||
| val method: String, | ||
| val url: String, | ||
| val statusCode: Int?, | ||
| val durationMs: Long, | ||
| val errorTitle: String? = null, | ||
| val errorMessage: String? = null, | ||
| ) | ||
|
|
||
| /** Keeps only the fields a crash record renders — see [NetworkRequestSummary]. */ | ||
| internal fun NetworkRequest.toSummary() = NetworkRequestSummary( | ||
| timestampMs = timestampMs, | ||
| method = method, | ||
| url = url, | ||
| statusCode = statusCode, | ||
| durationMs = durationMs, | ||
| errorTitle = error?.title, | ||
| errorMessage = error?.message | ||
| ) | ||
|
|
||
| /** A [CrashRecord] paired with the file it was loaded from. */ | ||
| internal data class CrashRecordInfo(val filePath: String, val record: CrashRecord) { | ||
| val file: File get() = File(filePath) | ||
| } | ||
39 changes: 39 additions & 0 deletions
39
...rlay-core/src/main/kotlin/com/ms/square/debugoverlay/internal/crash/CrashRecordBuilder.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package com.ms.square.debugoverlay.internal.crash | ||
|
|
||
| import com.ms.square.debugoverlay.internal.bugreport.model.AppInfo | ||
| import com.ms.square.debugoverlay.internal.bugreport.model.CustomLogSourceData | ||
| import com.ms.square.debugoverlay.model.LogEntry | ||
| import com.ms.square.debugoverlay.model.NetworkRequest | ||
|
|
||
| internal const val DEFAULT_MAX_LOG_LINES = 100 | ||
|
|
||
| /** | ||
| * Assembles a [CrashRecord] from already-captured, in-memory data. | ||
| * | ||
| * Pure and non-suspending: every input is passed in, so this can run on the crashing thread | ||
| * and can be tested without a [android.content.Context]. Each log/request source is trimmed | ||
| * to the last [maxLogLines] entries, and network requests are reduced to | ||
| * [NetworkRequestSummary] so bodies never reach disk. | ||
| */ | ||
| internal fun buildCrashRecord( | ||
| thread: Thread, | ||
| throwable: Throwable, | ||
| appInfo: AppInfo?, | ||
| logcatLogs: List<LogEntry>, | ||
| customLogSourceData: CustomLogSourceData?, | ||
| networkRequests: List<NetworkRequest>, | ||
| maxLogLines: Int = DEFAULT_MAX_LOG_LINES, | ||
| ): CrashRecord { | ||
| val maxLines = maxLogLines.coerceAtLeast(0) | ||
| return CrashRecord( | ||
| timestampMs = System.currentTimeMillis(), | ||
| threadName = thread.name, | ||
| exceptionType = throwable.javaClass.name, | ||
| message = throwable.message, | ||
| stackTrace = throwable.stackTraceToString(), | ||
| appInfo = appInfo, | ||
| logcatLogs = logcatLogs.takeLast(maxLines), | ||
| customLogSourceData = customLogSourceData?.let { it.copy(logs = it.logs.takeLast(maxLines)) }, | ||
| networkRequests = networkRequests.takeLast(maxLines).map { it.toSummary() } | ||
| ) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.