feat(diagnostics): harden ingestion and delivery

This commit is contained in:
2026-07-15 00:43:49 +02:00
parent ead4e09a60
commit b602a3acb6
42 changed files with 22428 additions and 141 deletions

View File

@@ -13,8 +13,16 @@ private class AndroidPendingCrashStore(
@Synchronized
override fun write(report: CrashReport) {
if (!isValidDiagnosticId(report.id)) return
directory.mkdirs()
File(directory, "${report.id}.crash").writeText(CrashReportCodec.encode(report), StandardCharsets.UTF_8)
val target = File(directory, "${report.id}.crash")
val temporary = File(directory, ".${report.id}.tmp")
val payload = CrashReportCodec.encode(report)
temporary.writeText(payload, StandardCharsets.UTF_8)
if (!temporary.renameTo(target)) {
target.writeText(payload, StandardCharsets.UTF_8)
temporary.delete()
}
}
@Synchronized
@@ -31,6 +39,34 @@ private class AndroidPendingCrashStore(
@Synchronized
override fun delete(id: String) {
if (!isValidDiagnosticId(id)) return
File(directory, "$id.crash").delete()
}
@Synchronized
override fun prune(olderThanTimestampMillis: Long, maxCount: Int) {
require(maxCount > 0) { "maxCount must be positive" }
if (!directory.isDirectory) return
directory.listFiles { file -> file.isFile && file.name.endsWith(".tmp") }
.orEmpty()
.forEach(File::delete)
val reports = directory
.listFiles { file -> file.isFile && file.name.endsWith(".crash") }
.orEmpty()
.mapNotNull { file ->
val report = runCatching {
CrashReportCodec.decode(file.readText(StandardCharsets.UTF_8))
}.getOrNull()
if (report == null) {
file.delete()
null
} else {
file to report
}
}
.sortedByDescending { (_, report) -> report.timestampMillis }
reports.forEachIndexed { index, (file, report) ->
if (index >= maxCount || report.timestampMillis < olderThanTimestampMillis) file.delete()
}
}
}

View File

@@ -0,0 +1,37 @@
package com.vnidrop.app.diagnostics
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URI
import java.nio.charset.StandardCharsets
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
actual suspend fun platformHttpPost(
url: String,
headers: Map<String, String>,
bodyUtf8: String,
): PlatformHttpResponse = withContext(Dispatchers.IO) {
val connection = (URI(url).toURL().openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
doOutput = true
connectTimeout = 15_000
readTimeout = 30_000
setRequestProperty("Content-Type", "application/json; charset=utf-8")
headers.forEach { (key, value) -> setRequestProperty(key, value) }
}
try {
connection.outputStream.use { output ->
output.write(bodyUtf8.toByteArray(StandardCharsets.UTF_8))
}
val code = connection.responseCode
val stream = if (code in 200..299) connection.inputStream else connection.errorStream
val body = stream?.use { input ->
BufferedReader(InputStreamReader(input, StandardCharsets.UTF_8)).readText()
}.orEmpty()
PlatformHttpResponse(code, body)
} finally {
connection.disconnect()
}
}