mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
refactor(diagnostics): remove bug report breadcrumbs
This commit is contained in:
@@ -1,47 +0,0 @@
|
||||
package com.vnidrop.app.diagnostics
|
||||
|
||||
import com.vnidrop.app.logging.platformNowMillis
|
||||
import kotlin.concurrent.atomics.AtomicReference
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
|
||||
/**
|
||||
* Fixed-size ring of high-level app breadcrumbs for crash / bug context.
|
||||
* Always in-memory only; never auto-uploaded without policy + transport.
|
||||
*
|
||||
* Updates are best-effort under concurrency; losing a breadcrumb is preferable
|
||||
* to blocking a dying process on a lock.
|
||||
*/
|
||||
@OptIn(ExperimentalAtomicApi::class)
|
||||
class BreadcrumbBuffer(
|
||||
private val capacity: Int = DefaultCapacity,
|
||||
) {
|
||||
init {
|
||||
require(capacity > 0) { "capacity must be positive" }
|
||||
}
|
||||
|
||||
private val items = AtomicReference<List<Breadcrumb>>(emptyList())
|
||||
|
||||
fun add(name: String, properties: Map<String, String> = emptyMap(), timestampMillis: Long = platformNowMillis()) {
|
||||
val sanitizedName = sanitizeDiagnosticName(name)
|
||||
if (sanitizedName.isBlank()) return
|
||||
val crumb = Breadcrumb(
|
||||
name = sanitizedName,
|
||||
timestampMillis = timestampMillis,
|
||||
properties = sanitizeDiagnosticProperties(properties),
|
||||
)
|
||||
while (true) {
|
||||
val current = items.load()
|
||||
if (items.compareAndSet(current, (current + crumb).takeLast(capacity))) return
|
||||
}
|
||||
}
|
||||
|
||||
fun snapshot(): List<Breadcrumb> = items.load()
|
||||
|
||||
fun clear() {
|
||||
items.store(emptyList())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DefaultCapacity = 40
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ data class BugReportDraft(
|
||||
class BugReportService(
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val transport: DiagnosticsTransport,
|
||||
private val breadcrumbs: BreadcrumbBuffer,
|
||||
private val appVersion: String,
|
||||
private val platform: String,
|
||||
private val logReader: () -> String = {
|
||||
@@ -53,7 +52,6 @@ class BugReportService(
|
||||
network = deviceInfo?.network?.takeUtf8Bytes(96),
|
||||
batteryLevel = deviceInfo?.batteryLevel?.takeUtf8Bytes(64),
|
||||
),
|
||||
breadcrumbs = breadcrumbs.snapshot(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import kotlinx.coroutines.launch
|
||||
class DiagnosticsCoordinator(
|
||||
val preferencesRepository: PreferencesRepository,
|
||||
val transport: DiagnosticsTransport,
|
||||
val breadcrumbs: BreadcrumbBuffer,
|
||||
val bugReports: BugReportService,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
@@ -32,18 +31,15 @@ class DiagnosticsCoordinator(
|
||||
scope: CoroutineScope,
|
||||
transport: DiagnosticsTransport = NoOpDiagnosticsTransport(),
|
||||
): DiagnosticsCoordinator {
|
||||
val breadcrumbs = BreadcrumbBuffer()
|
||||
val bugReports = BugReportService(
|
||||
preferencesRepository = preferencesRepository,
|
||||
transport = transport,
|
||||
breadcrumbs = breadcrumbs,
|
||||
appVersion = appVersion,
|
||||
platform = platform,
|
||||
)
|
||||
return DiagnosticsCoordinator(
|
||||
preferencesRepository = preferencesRepository,
|
||||
transport = transport,
|
||||
breadcrumbs = breadcrumbs,
|
||||
bugReports = bugReports,
|
||||
scope = scope,
|
||||
)
|
||||
|
||||
@@ -8,8 +8,6 @@ internal object DiagnosticsJson {
|
||||
internal const val MaxInstallIdBytes = 80
|
||||
internal const val MaxAppVersionBytes = 40
|
||||
internal const val MaxPlatformBytes = 40
|
||||
private const val MaxBreadcrumbsJsonBytes = 16_000
|
||||
private const val MaxBreadcrumbs = 40
|
||||
|
||||
fun bugBody(report: BugReport): String {
|
||||
val logs = if (report.includeLogs) report.logs else ""
|
||||
@@ -72,47 +70,7 @@ internal object DiagnosticsJson {
|
||||
appendJsonField("network", report.device.network.orEmpty())
|
||||
append(',')
|
||||
appendJsonField("batteryLevel", report.device.batteryLevel.orEmpty())
|
||||
append("},")
|
||||
append("\"breadcrumbs\":")
|
||||
appendBreadcrumbs(report.breadcrumbs)
|
||||
append('}')
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendBreadcrumbs(crumbs: List<Breadcrumb>) {
|
||||
append('[')
|
||||
var encodedBytes = 2
|
||||
var appended = 0
|
||||
for (crumb in crumbs) {
|
||||
if (appended == MaxBreadcrumbs) break
|
||||
val name = sanitizeDiagnosticName(crumb.name)
|
||||
if (name.isBlank() || crumb.timestampMillis < 0) continue
|
||||
val encoded = buildString {
|
||||
append('{')
|
||||
appendJsonField("name", name)
|
||||
append(',')
|
||||
append("\"timestampMillis\":")
|
||||
append(crumb.timestampMillis)
|
||||
append(',')
|
||||
append("\"properties\":")
|
||||
appendStringMap(crumb.properties)
|
||||
append('}')
|
||||
}
|
||||
val additionBytes = encoded.encodeToByteArray().size + if (appended == 0) 0 else 1
|
||||
if (encodedBytes + additionBytes > MaxBreadcrumbsJsonBytes) break
|
||||
if (appended > 0) append(',')
|
||||
append(encoded)
|
||||
encodedBytes += additionBytes
|
||||
appended += 1
|
||||
}
|
||||
append(']')
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendStringMap(map: Map<String, String>) {
|
||||
append('{')
|
||||
sanitizeDiagnosticProperties(map).entries.forEachIndexed { index, (key, value) ->
|
||||
if (index > 0) append(',')
|
||||
appendJsonField(key, value)
|
||||
}
|
||||
append('}')
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,6 @@ package com.vnidrop.app.diagnostics
|
||||
* intentionally abstracted; nothing here assumes a network backend.
|
||||
*/
|
||||
|
||||
data class Breadcrumb(
|
||||
val name: String,
|
||||
val timestampMillis: Long,
|
||||
val properties: Map<String, String> = emptyMap(),
|
||||
)
|
||||
|
||||
data class DeviceSnapshot(
|
||||
val deviceName: String?,
|
||||
val deviceModel: String?,
|
||||
@@ -32,7 +26,6 @@ data class BugReport(
|
||||
val includeLogs: Boolean,
|
||||
val logs: String,
|
||||
val device: DeviceSnapshot,
|
||||
val breadcrumbs: List<Breadcrumb>,
|
||||
val schemaVersion: Int = DiagnosticsSchemaVersion,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
package com.vnidrop.app.diagnostics
|
||||
|
||||
internal const val MaxDiagnosticProperties = 12
|
||||
internal const val MaxDiagnosticPropertyKeyBytes = 40
|
||||
internal const val MaxDiagnosticPropertyValueBytes = 128
|
||||
internal const val MaxDiagnosticNameBytes = 64
|
||||
|
||||
internal fun sanitizeDiagnosticsInstallId(value: String): String {
|
||||
val trimmed = value.trim()
|
||||
if (trimmed.any { it.code < 0x20 || it.code == 0x7f }) return ""
|
||||
return trimmed.takeUtf8Bytes(DiagnosticsJson.MaxInstallIdBytes)
|
||||
}
|
||||
|
||||
internal fun sanitizeDiagnosticName(name: String): String =
|
||||
name.takeUtf8Bytes(MaxDiagnosticNameBytes)
|
||||
|
||||
internal fun sanitizeDiagnosticProperties(properties: Map<String, String>): Map<String, String> {
|
||||
val sanitized = LinkedHashMap<String, String>(minOf(properties.size, MaxDiagnosticProperties))
|
||||
for ((rawKey, rawValue) in properties) {
|
||||
val key = rawKey.takeUtf8Bytes(MaxDiagnosticPropertyKeyBytes)
|
||||
if (key.isEmpty() || key in sanitized) continue
|
||||
sanitized[key] = LogRedactor.redact(rawValue).takeUtf8Bytes(MaxDiagnosticPropertyValueBytes)
|
||||
if (sanitized.size == MaxDiagnosticProperties) break
|
||||
}
|
||||
return sanitized
|
||||
}
|
||||
|
||||
internal fun String.takeUtf8Bytes(maxBytes: Int): String {
|
||||
require(maxBytes >= 0) { "maxBytes must not be negative" }
|
||||
val encoded = encodeToByteArray()
|
||||
|
||||
@@ -204,22 +204,11 @@ class DiagnosticsTest {
|
||||
assertTrue(redacted.contains("[redacted-endpoint]"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun breadcrumbBufferKeepsOnlyLatestEntries() {
|
||||
val buffer = BreadcrumbBuffer(capacity = 3)
|
||||
buffer.add("a")
|
||||
buffer.add("b")
|
||||
buffer.add("c")
|
||||
buffer.add("d")
|
||||
assertEquals(listOf("b", "c", "d"), buffer.snapshot().map { it.name })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bugReportRequiresWhatAndExpected() = runTest {
|
||||
val service = BugReportService(
|
||||
preferencesRepository = fakePrefs(),
|
||||
transport = RecordingDiagnosticsTransport(),
|
||||
breadcrumbs = BreadcrumbBuffer(),
|
||||
appVersion = "1.0",
|
||||
platform = "Test",
|
||||
logReader = { "logs" },
|
||||
@@ -238,7 +227,6 @@ class DiagnosticsTest {
|
||||
val service = BugReportService(
|
||||
preferencesRepository = fakePrefs(),
|
||||
transport = throwingTransport,
|
||||
breadcrumbs = BreadcrumbBuffer(),
|
||||
appVersion = "1.0",
|
||||
platform = "Test",
|
||||
)
|
||||
@@ -255,7 +243,6 @@ class DiagnosticsTest {
|
||||
val service = BugReportService(
|
||||
preferencesRepository = fakePrefs(),
|
||||
transport = transport,
|
||||
breadcrumbs = BreadcrumbBuffer(),
|
||||
appVersion = "1.0",
|
||||
platform = "Test",
|
||||
logReader = { LogRedactor.redact("ticket=abcdefghijklmnopqrstuvwxyz012345 plain") },
|
||||
@@ -286,7 +273,6 @@ class DiagnosticsTest {
|
||||
val service = BugReportService(
|
||||
preferencesRepository = fakePrefs(),
|
||||
transport = RecordingDiagnosticsTransport(),
|
||||
breadcrumbs = BreadcrumbBuffer(),
|
||||
appVersion = "1.0",
|
||||
platform = "Test",
|
||||
logReader = { rawLogs },
|
||||
@@ -304,7 +290,6 @@ class DiagnosticsTest {
|
||||
val service = BugReportService(
|
||||
preferencesRepository = fakePrefs(),
|
||||
transport = RecordingDiagnosticsTransport(),
|
||||
breadcrumbs = BreadcrumbBuffer(),
|
||||
appVersion = "1.0",
|
||||
platform = "Test",
|
||||
logReader = { "🙂".repeat(60_000) },
|
||||
@@ -333,7 +318,6 @@ class DiagnosticsTest {
|
||||
includeLogs = includeLogs,
|
||||
logs = logs,
|
||||
device = DeviceSnapshot(null, null, "OS", null, null),
|
||||
breadcrumbs = emptyList(),
|
||||
)
|
||||
|
||||
private fun fakePrefs() = FakePreferencesRepository(
|
||||
|
||||
@@ -18,7 +18,6 @@ import com.vnidrop.app.core.ShareAccessPolicy
|
||||
import com.vnidrop.app.core.Transfer
|
||||
import com.vnidrop.app.core.TransferDirection
|
||||
import com.vnidrop.app.core.TransferStatus
|
||||
import com.vnidrop.app.diagnostics.BreadcrumbBuffer
|
||||
import com.vnidrop.app.diagnostics.BugReportService
|
||||
import com.vnidrop.app.diagnostics.DiagnosticsTransport
|
||||
import com.vnidrop.app.diagnostics.NoOpDiagnosticsTransport
|
||||
@@ -989,7 +988,6 @@ class ViewModelsTest {
|
||||
BugReportService(
|
||||
preferencesRepository = preferences,
|
||||
transport = transport,
|
||||
breadcrumbs = BreadcrumbBuffer(),
|
||||
appVersion = "1.0",
|
||||
platform = "Test",
|
||||
logReader = { "sample log line" },
|
||||
|
||||
Reference in New Issue
Block a user