fix(kotlin-sdk): thread-safety, resource leaks, URL encoding, JSON safety, deprecated API

- BLTelemetryClient/BLAuditLogger/BLCrashReporter: @Synchronized isoNow() for SimpleDateFormat thread-safety

- BLCrashReporter: replace unsafe JSON string interpolation with buildJsonObject + JsonArray

- BLBlobClient: close OkHttp Response body with .use {} to prevent resource leak

- BLFeatureFlagClient/BLKillSwitchClient: URL-encode query parameter values

- build.gradle.kts: kotlinOptions {} -> compilerOptions {} (Kotlin 2.1 convention)
This commit is contained in:
saravanakumardb1 2026-03-01 21:17:27 -08:00
parent 4f16223996
commit f953c2b0bc
7 changed files with 34 additions and 12 deletions

View File

@ -19,8 +19,8 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}

View File

@ -43,6 +43,10 @@ class BLAuditLogger(
timeZone = TimeZone.getTimeZone("UTC")
}
/** Thread-safe ISO timestamp — SimpleDateFormat is NOT thread-safe. */
@Synchronized
private fun isoNow(): String = isoFormat.format(Date())
init {
logDir.mkdirs()
}
@ -52,7 +56,7 @@ class BLAuditLogger(
*/
fun log(action: String, module: String, detail: String? = null, userId: String? = null) {
val entry = AuditEntry(
timestamp = isoFormat.format(Date()),
timestamp = isoNow(),
productId = config.productId,
action = action,
module = module,

View File

@ -77,8 +77,9 @@ class BLBlobClient(
.header("x-ms-blob-content-type", contentType)
.build()
val response = uploadClient.newCall(request).execute()
if (response.isSuccessful) sas.blobUrl else null
uploadClient.newCall(request).execute().use { response ->
if (response.isSuccessful) sas.blobUrl else null
}
} catch (_: Exception) {
null
}

View File

@ -8,6 +8,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.io.PrintWriter
@ -42,6 +43,10 @@ class BLCrashReporter(
timeZone = TimeZone.getTimeZone("UTC")
}
/** Thread-safe ISO timestamp — SimpleDateFormat is NOT thread-safe. */
@Synchronized
private fun isoNow(): String = isoFormat.format(Date())
companion object {
private const val KEY_PENDING_CRASH = "pending_crash"
}
@ -66,7 +71,7 @@ class BLCrashReporter(
put("id", UUID.randomUUID().toString())
put("productId", config.productId)
put("platform", config.platform)
put("timestamp", isoFormat.format(Date()))
put("timestamp", isoNow())
put("thread", thread.name)
put("exception", throwable.javaClass.name)
put("message", throwable.message ?: "")
@ -87,8 +92,12 @@ class BLCrashReporter(
scope.launch {
try {
val body = """{"productId":"${config.productId}","events":[$pending]}"""
client.fireAndForget("POST", "/telemetry/events", body)
val crashEvent = json.parseToJsonElement(pending)
val payload = buildJsonObject {
put("productId", config.productId)
put("events", JsonArray(listOf(crashEvent)))
}
client.fireAndForget("POST", "/telemetry/events", json.encodeToString(payload))
} catch (_: Exception) {
// Best-effort — don't re-queue
}

View File

@ -9,6 +9,7 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.net.URLEncoder
/**
* Feature flag client for platform-service.
@ -76,9 +77,10 @@ class BLFeatureFlagClient(
private suspend fun fetchFlags() {
try {
val enc = { v: String -> URLEncoder.encode(v, "UTF-8") }
val qs = buildString {
append("?platform=${config.platform}")
userId?.let { append("&userId=$it") }
append("?platform=${enc(config.platform)}")
userId?.let { append("&userId=${enc(it)}") }
}
val response = client.request("GET", "/flags/poll$qs", skipAuth = true)
val result = json.decodeFromString<FlagResponse>(response)

View File

@ -2,6 +2,7 @@ package com.bytelyst.platform
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.net.URLEncoder
/**
* Kill switch client for platform-service.
@ -32,7 +33,8 @@ class BLKillSwitchClient(
*/
suspend fun check(): KillSwitchResult {
return try {
val response = client.request("GET", "/flags/kill-switch?platform=${config.platform}", skipAuth = true)
val platform = URLEncoder.encode(config.platform, "UTF-8")
val response = client.request("GET", "/flags/kill-switch?platform=$platform", skipAuth = true)
json.decodeFromString<KillSwitchResult>(response)
} catch (_: Exception) {
KillSwitchResult.ok()

View File

@ -99,6 +99,10 @@ class BLTelemetryClient(
timeZone = TimeZone.getTimeZone("UTC")
}
/** Thread-safe ISO timestamp — SimpleDateFormat is NOT thread-safe. */
@Synchronized
private fun isoNow(): String = isoFormat.format(Date())
// ── Lifecycle ────────────────────────────────────────────
fun start() {
@ -155,7 +159,7 @@ class BLTelemetryClient(
errorDomain = errorDomain,
tags = tags,
metrics = metrics,
occurredAt = isoFormat.format(Date()),
occurredAt = isoNow(),
)
synchronized(queue) {