diff --git a/Confidence/src/main/java/com/spotify/confidence/Telemetry.kt b/Confidence/src/main/java/com/spotify/confidence/Telemetry.kt index 10104e12..440e60be 100644 --- a/Confidence/src/main/java/com/spotify/confidence/Telemetry.kt +++ b/Confidence/src/main/java/com/spotify/confidence/Telemetry.kt @@ -32,66 +32,68 @@ internal class Telemetry( } fun encodedHeaderValue(): String? { - val (evalSnapshot, resolveSnapshot) = synchronized(lock) { - val evals = evaluationTraces.toList() - val resolves = resolveLatencyTraces.toList() + val snapshot = synchronized(lock) { + val s = Snapshot( + evaluations = evaluationTraces.toList(), + resolveTraces = resolveLatencyTraces.toList(), + library = library + ) evaluationTraces.clear() resolveLatencyTraces.clear() - Pair(evals, resolves) + s } - if (evalSnapshot.isEmpty() && resolveSnapshot.isEmpty()) return null + if (snapshot.evaluations.isEmpty() && snapshot.resolveTraces.isEmpty()) return null - val bytes = encodeMonitoring(evalSnapshot, resolveSnapshot) + val bytes = encodeMonitoring(snapshot) return Base64.encodeToString(bytes, Base64.NO_WRAP) } - private fun encodeMonitoring( - evals: List, - resolves: List - ): ByteArray { + private data class Snapshot( + val evaluations: List, + val resolveTraces: List, + val library: Library + ) + + private fun encodeMonitoring(snapshot: Snapshot): ByteArray { val out = ByteArrayOutputStream() - // field 1: LibraryTraces (length-delimited) - val libraryTracesBytes = encodeLibraryTraces(evals, resolves) + val libraryTracesBytes = encodeLibraryTraces(snapshot) out.writeTag(1, WIRE_TYPE_LENGTH_DELIMITED) - out.writeVarint(libraryTracesBytes.size) + out.writeVarint(libraryTracesBytes.size.toLong()) out.write(libraryTracesBytes) - // field 2: platform (varint) - KOTLIN = 2 - out.writeTag(2, WIRE_TYPE_VARINT) - out.writeVarint(Platform.KOTLIN.value) + if (Platform.KOTLIN.value != 0) { + out.writeTag(2, WIRE_TYPE_VARINT) + out.writeVarint(Platform.KOTLIN.value.toLong()) + } return out.toByteArray() } - private fun encodeLibraryTraces( - evals: List, - resolves: List - ): ByteArray { + private fun encodeLibraryTraces(snapshot: Snapshot): ByteArray { val out = ByteArrayOutputStream() - // field 1: library (varint) - out.writeTag(1, WIRE_TYPE_VARINT) - out.writeVarint(library.value) + if (snapshot.library.value != 0) { + out.writeTag(1, WIRE_TYPE_VARINT) + out.writeVarint(snapshot.library.value.toLong()) + } - // field 2: library_version (string) - out.writeTag(2, WIRE_TYPE_LENGTH_DELIMITED) val versionBytes = sdkVersion.toByteArray(Charsets.UTF_8) - out.writeVarint(versionBytes.size) + out.writeTag(2, WIRE_TYPE_LENGTH_DELIMITED) + out.writeVarint(versionBytes.size.toLong()) out.write(versionBytes) - // field 3: traces (repeated) - for (resolve in resolves) { + for (resolve in snapshot.resolveTraces) { val traceBytes = encodeResolveLatencyTrace(resolve) out.writeTag(3, WIRE_TYPE_LENGTH_DELIMITED) - out.writeVarint(traceBytes.size) + out.writeVarint(traceBytes.size.toLong()) out.write(traceBytes) } - for (eval in evals) { + for (eval in snapshot.evaluations) { val traceBytes = encodeEvaluationTrace(eval) out.writeTag(3, WIRE_TYPE_LENGTH_DELIMITED) - out.writeVarint(traceBytes.size) + out.writeVarint(traceBytes.size.toLong()) out.write(traceBytes) } @@ -101,14 +103,12 @@ internal class Telemetry( private fun encodeResolveLatencyTrace(trace: ResolveLatencyTrace): ByteArray { val out = ByteArrayOutputStream() - // field 1: id (varint) - RESOLVE_LATENCY = 1 out.writeTag(1, WIRE_TYPE_VARINT) - out.writeVarint(TraceId.RESOLVE_LATENCY.value) + out.writeVarint(TraceId.RESOLVE_LATENCY.value.toLong()) - // field 3: request_trace (length-delimited) - oneof field 3 val requestTraceBytes = encodeRequestTrace(trace) out.writeTag(3, WIRE_TYPE_LENGTH_DELIMITED) - out.writeVarint(requestTraceBytes.size) + out.writeVarint(requestTraceBytes.size.toLong()) out.write(requestTraceBytes) return out.toByteArray() @@ -117,13 +117,15 @@ internal class Telemetry( private fun encodeRequestTrace(trace: ResolveLatencyTrace): ByteArray { val out = ByteArrayOutputStream() - // field 1: latency_ms (varint) - out.writeTag(1, WIRE_TYPE_VARINT) - out.writeVarint(trace.durationMs.toInt()) + if (trace.durationMs != 0L) { + out.writeTag(1, WIRE_TYPE_VARINT) + out.writeVarint(trace.durationMs) + } - // field 2: status (varint) - out.writeTag(2, WIRE_TYPE_VARINT) - out.writeVarint(trace.status.value) + if (trace.status.value != 0) { + out.writeTag(2, WIRE_TYPE_VARINT) + out.writeVarint(trace.status.value.toLong()) + } return out.toByteArray() } @@ -131,15 +133,15 @@ internal class Telemetry( private fun encodeEvaluationTrace(trace: EvaluationTrace): ByteArray { val out = ByteArrayOutputStream() - // field 1: id (varint) - FLAG_EVALUATION = 3 out.writeTag(1, WIRE_TYPE_VARINT) - out.writeVarint(TraceId.FLAG_EVALUATION.value) + out.writeVarint(TraceId.FLAG_EVALUATION.value.toLong()) - // field 5: evaluation_trace (length-delimited) - oneof field 5 val evalTraceBytes = encodeEvalTraceBody(trace) - out.writeTag(5, WIRE_TYPE_LENGTH_DELIMITED) - out.writeVarint(evalTraceBytes.size) - out.write(evalTraceBytes) + if (evalTraceBytes.isNotEmpty()) { + out.writeTag(5, WIRE_TYPE_LENGTH_DELIMITED) + out.writeVarint(evalTraceBytes.size.toLong()) + out.write(evalTraceBytes) + } return out.toByteArray() } @@ -147,13 +149,15 @@ internal class Telemetry( private fun encodeEvalTraceBody(trace: EvaluationTrace): ByteArray { val out = ByteArrayOutputStream() - // field 1: reason (varint) - out.writeTag(1, WIRE_TYPE_VARINT) - out.writeVarint(trace.reason.value) + if (trace.reason.value != 0) { + out.writeTag(1, WIRE_TYPE_VARINT) + out.writeVarint(trace.reason.value.toLong()) + } - // field 2: error_code (varint) - out.writeTag(2, WIRE_TYPE_VARINT) - out.writeVarint(trace.errorCode.value) + if (trace.errorCode.value != 0) { + out.writeTag(2, WIRE_TYPE_VARINT) + out.writeVarint(trace.errorCode.value.toLong()) + } return out.toByteArray() } @@ -204,16 +208,16 @@ internal class Telemetry( } private fun ByteArrayOutputStream.writeTag(fieldNumber: Int, wireType: Int) { - writeVarint((fieldNumber shl 3) or wireType) + writeVarint(((fieldNumber shl 3) or wireType).toLong()) } - private fun ByteArrayOutputStream.writeVarint(value: Int) { + private fun ByteArrayOutputStream.writeVarint(value: Long) { var v = value - while (v and 0x7F.inv() != 0) { - write((v and 0x7F) or 0x80) + while (v and 0x7FL.inv() != 0L) { + write(((v and 0x7F) or 0x80).toInt()) v = v ushr 7 } - write(v) + write(v.toInt()) } } @@ -244,6 +248,9 @@ internal class Telemetry( DEFAULT(2), STALE(3), DISABLED(4), + CACHED(5), + STATIC(6), + SPLIT(7), ERROR(8) } @@ -252,8 +259,10 @@ internal class Telemetry( PROVIDER_NOT_READY(1), FLAG_NOT_FOUND(2), PARSE_ERROR(3), + TYPE_MISMATCH(4), TARGETING_KEY_MISSING(5), INVALID_CONTEXT(6), + PROVIDER_FATAL(7), GENERAL(8) } diff --git a/Confidence/src/test/java/com/spotify/confidence/ConfidenceRemoteClientTests.kt b/Confidence/src/test/java/com/spotify/confidence/ConfidenceRemoteClientTests.kt index 0457a847..3c449cb8 100644 --- a/Confidence/src/test/java/com/spotify/confidence/ConfidenceRemoteClientTests.kt +++ b/Confidence/src/test/java/com/spotify/confidence/ConfidenceRemoteClientTests.kt @@ -825,7 +825,6 @@ internal class ConfidenceRemoteClientTests { whenever(mockClock.currentTime()).thenReturn(sendDate) val telemetry = Telemetry(SDK_ID + "_TEST", Telemetry.Library.CONFIDENCE, "1.0.0") - // No events tracked var recordedRequest: RecordedRequest? = null mockWebServer.dispatcher = object : Dispatcher() { diff --git a/Confidence/src/test/java/com/spotify/confidence/TelemetryTest.kt b/Confidence/src/test/java/com/spotify/confidence/TelemetryTest.kt index ac6f8cd7..b45bed55 100644 --- a/Confidence/src/test/java/com/spotify/confidence/TelemetryTest.kt +++ b/Confidence/src/test/java/com/spotify/confidence/TelemetryTest.kt @@ -224,7 +224,7 @@ class TelemetryTest { Telemetry.EvaluationErrorCode.UNSPECIFIED ) - val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!) + val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()) assertEquals(ProtoPlatform.PLATFORM_KOTLIN, monitoring.platform) assertEquals(1, monitoring.libraryTracesCount) @@ -248,7 +248,7 @@ class TelemetryTest { val telemetry = Telemetry("test-sdk", Telemetry.Library.CONFIDENCE, "2.0.0") telemetry.trackResolveLatency(142, Telemetry.RequestStatus.SUCCESS) - val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!) + val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()) assertEquals(ProtoPlatform.PLATFORM_KOTLIN, monitoring.platform) @@ -273,7 +273,7 @@ class TelemetryTest { Telemetry.EvaluationErrorCode.FLAG_NOT_FOUND ) - val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!) + val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()) val lib = monitoring.getLibraryTraces(0) assertEquals(LibraryTraces.Library.LIBRARY_OPEN_FEATURE, lib.library) @@ -298,7 +298,7 @@ class TelemetryTest { Telemetry.EvaluationErrorCode.UNSPECIFIED ) - val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!) + val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()) val traces = monitoring.getLibraryTraces(0).tracesList assertEquals(4, traces.size) @@ -324,7 +324,7 @@ class TelemetryTest { val telemetry = Telemetry("test-sdk", Telemetry.Library.CONFIDENCE, "1.0.0") telemetry.trackResolveLatency(250, Telemetry.RequestStatus.ERROR) - val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!) + val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()) val req = monitoring.getLibraryTraces(0).getTraces(0).requestTrace assertEquals(250L, req.millisecondDuration) assertEquals(ProtoStatus.STATUS_ERROR, req.status) @@ -370,6 +370,24 @@ class TelemetryTest { ProtoReason.EVALUATION_REASON_DISABLED, ProtoErrorCode.EVALUATION_ERROR_CODE_UNSPECIFIED ), + Case( + Telemetry.EvaluationReason.CACHED, + Telemetry.EvaluationErrorCode.UNSPECIFIED, + ProtoReason.EVALUATION_REASON_CACHED, + ProtoErrorCode.EVALUATION_ERROR_CODE_UNSPECIFIED + ), + Case( + Telemetry.EvaluationReason.STATIC, + Telemetry.EvaluationErrorCode.UNSPECIFIED, + ProtoReason.EVALUATION_REASON_STATIC, + ProtoErrorCode.EVALUATION_ERROR_CODE_UNSPECIFIED + ), + Case( + Telemetry.EvaluationReason.SPLIT, + Telemetry.EvaluationErrorCode.UNSPECIFIED, + ProtoReason.EVALUATION_REASON_SPLIT, + ProtoErrorCode.EVALUATION_ERROR_CODE_UNSPECIFIED + ), Case( Telemetry.EvaluationReason.ERROR, Telemetry.EvaluationErrorCode.GENERAL, @@ -405,6 +423,18 @@ class TelemetryTest { Telemetry.EvaluationErrorCode.TARGETING_KEY_MISSING, ProtoReason.EVALUATION_REASON_ERROR, ProtoErrorCode.EVALUATION_ERROR_CODE_TARGETING_KEY_MISSING + ), + Case( + Telemetry.EvaluationReason.ERROR, + Telemetry.EvaluationErrorCode.TYPE_MISMATCH, + ProtoReason.EVALUATION_REASON_ERROR, + ProtoErrorCode.EVALUATION_ERROR_CODE_TYPE_MISMATCH + ), + Case( + Telemetry.EvaluationReason.ERROR, + Telemetry.EvaluationErrorCode.PROVIDER_FATAL, + ProtoReason.EVALUATION_REASON_ERROR, + ProtoErrorCode.EVALUATION_ERROR_CODE_PROVIDER_FATAL ) ) @@ -412,7 +442,7 @@ class TelemetryTest { val telemetry = Telemetry("test-sdk", Telemetry.Library.CONFIDENCE, "1.0.0") telemetry.trackEvaluation(case.reason, case.errorCode) - val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!) + val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()) val eval = monitoring.getLibraryTraces(0).getTraces(0).evaluationTrace assertEquals("reason for ${case.reason}", case.expectedReason, eval.reason) @@ -449,17 +479,14 @@ class TelemetryTest { debugLogger = null ) - // Default should be CONFIDENCE assertEquals(Telemetry.Library.CONFIDENCE, confidence.telemetry.library) - // Call the private method via reflection (same as ConfidenceFeatureProvider.create) val method = confidence.javaClass.getDeclaredMethod("setTelemetryLibraryOpenFeature") method.isAccessible = true method.invoke(confidence) assertEquals(Telemetry.Library.OPEN_FEATURE, confidence.telemetry.library) - // Verify encoded header uses OPEN_FEATURE confidence.telemetry.trackEvaluation( Telemetry.EvaluationReason.DEFAULT, Telemetry.EvaluationErrorCode.UNSPECIFIED @@ -532,7 +559,7 @@ class TelemetryTest { latch.await() executor.shutdown() - val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!) + val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()) val traces = monitoring.getLibraryTraces(0).tracesList // Each list is capped at 100, so max 200 total (100 eval + 100 latency) assertTrue("Traces should be capped at 200", traces.size <= 200) @@ -554,7 +581,7 @@ class TelemetryTest { telemetry.trackResolveLatency(50, Telemetry.RequestStatus.SUCCESS) } - val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()!!) + val monitoring = decodeMonitoring(telemetry.encodedHeaderValue()) val traces = monitoring.getLibraryTraces(0).tracesList val evalTraces = traces.filter { it.id == LibraryTraces.TraceId.TRACE_ID_FLAG_EVALUATION @@ -640,6 +667,7 @@ class TelemetryTest { assertNotNull("Expected ${Telemetry.HEADER_NAME} header", headerValue) val monitoring = decodeMonitoring(headerValue!!) + assertEquals(ProtoPlatform.PLATFORM_KOTLIN, monitoring.platform) val lib = monitoring.getLibraryTraces(0)