TelemetryClient.kt
package com.depanalyzer.telemetry
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import tools.jackson.databind.json.JsonMapper
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.TimeUnit
object TelemetryClient {
private val json = "application/json; charset=utf-8".toMediaType()
private val mapper = JsonMapper.builder().build()
private val pendingThreads = ConcurrentLinkedQueue<Thread>()
private val http = OkHttpClient.Builder()
.connectTimeout(6, TimeUnit.SECONDS)
.readTimeout(6, TimeUnit.SECONDS)
.writeTimeout(6, TimeUnit.SECONDS)
.build()
fun send(event: TelemetryEvent) {
if (!TelemetryConfig.enabled) return
val worker = Thread {
try {
val payload = linkedMapOf<String, Any?>(
"appId" to event.appId,
"appVersion" to event.appVersion,
"os" to event.os,
"eventType" to event.eventType,
"sessionId" to event.sessionId,
"arch" to event.arch,
"feature" to event.feature,
"durationMs" to event.durationMs,
"errorType" to event.errorType,
"errorMessage" to event.errorMessage
).filterValues { it != null }
val body = mapper.writeValueAsString(payload).toRequestBody(json)
val request = Request.Builder()
.url(TelemetryConfig.ingestUrl)
.post(body)
.build()
http.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
System.err.println("[telemetry] POST /ingest returned ${response.code} - skipping")
}
}
} catch (e: Exception) {
System.err.println("[telemetry] send failed silently: ${e.message}")
} finally {
pendingThreads.remove(Thread.currentThread())
}
}.also {
it.isDaemon = true
pendingThreads.add(it)
}
worker.start()
}
fun flush(timeoutMs: Long = 1500L) {
val deadline = System.currentTimeMillis() + timeoutMs
while (true) {
val active = pendingThreads.toList().filter { it.isAlive }
if (active.isEmpty()) {
return
}
val remaining = deadline - System.currentTimeMillis()
if (remaining <= 0L) {
return
}
active.forEach { thread ->
if (!thread.isAlive) return@forEach
val perThreadWait = minOf(remaining, 200L)
runCatching { thread.join(perThreadWait) }
}
}
}
}