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:
parent
4f16223996
commit
f953c2b0bc
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user