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 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { compilerOptions {
jvmTarget = "17" jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package com.bytelyst.platform
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.net.URLEncoder
/** /**
* Kill switch client for platform-service. * Kill switch client for platform-service.
@ -32,7 +33,8 @@ class BLKillSwitchClient(
*/ */
suspend fun check(): KillSwitchResult { suspend fun check(): KillSwitchResult {
return try { 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) json.decodeFromString<KillSwitchResult>(response)
} catch (_: Exception) { } catch (_: Exception) {
KillSwitchResult.ok() KillSwitchResult.ok()

View File

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