From b0d80b671247535717c49e0248b4678370fdd799 Mon Sep 17 00:00:00 2001 From: Fabrizio Demaria Date: Tue, 5 Nov 2024 16:18:35 +0100 Subject: [PATCH] feat: poc: Telemetry architecture --- .../Apply/FlagApplierWithRetries.swift | 4 ++- Sources/Confidence/Confidence.swift | 3 +- Sources/Confidence/FlagEvaluation.swift | 1 + Sources/Confidence/Http/HttpClient.swift | 1 + Sources/Confidence/Http/NetworkClient.swift | 24 ++++++++++++- .../Confidence/RemoteConfidenceClient.swift | 4 ++- .../RemoteResolveConfidenceClient.swift | 5 +-- .../Telemetry/TelemetryManager.swift | 35 +++++++++++++++++++ .../Telemetry/TelemetryPayload.swift | 5 +++ .../Helpers/HttpClientMock.swift | 4 +++ 10 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 Sources/Confidence/Telemetry/TelemetryManager.swift create mode 100644 Sources/Confidence/Telemetry/TelemetryPayload.swift diff --git a/Sources/Confidence/Apply/FlagApplierWithRetries.swift b/Sources/Confidence/Apply/FlagApplierWithRetries.swift index f9bada99..5c5296c9 100644 --- a/Sources/Confidence/Apply/FlagApplierWithRetries.swift +++ b/Sources/Confidence/Apply/FlagApplierWithRetries.swift @@ -11,6 +11,7 @@ final class FlagApplierWithRetries: FlagApplier { private let cacheDataInteractor: CacheDataActor private let metadata: ConfidenceMetadata private let debugLogger: DebugLogger? + private let telemetry = Telemetry.shared init( httpClient: HttpClient, @@ -139,7 +140,8 @@ final class FlagApplierWithRetries: FlagApplier { request: ApplyFlagsRequest ) async -> ApplyFlagResult { do { - return try await httpClient.post(path: ":apply", data: request) + let header = telemetry.getSnapshot() + return try await httpClient.post(path: ":apply", data: request, header: header) } catch { return .failure(handleError(error: error)) } diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index a5069082..f63f9d49 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -153,7 +153,8 @@ public class Confidence: ConfidenceEventSender { return self.cache.evaluate( flagName: key, defaultValue: defaultValue, - context: getContext(), + // TMP - TESTING (force a different context, causing STALE) + context: ["test":ConfidenceValue(null: ())], flagApplier: flagApplier ) } diff --git a/Sources/Confidence/FlagEvaluation.swift b/Sources/Confidence/FlagEvaluation.swift index 1193f4b4..86fc6d7c 100644 --- a/Sources/Confidence/FlagEvaluation.swift +++ b/Sources/Confidence/FlagEvaluation.swift @@ -74,6 +74,7 @@ extension FlagResolution { var resolveReason: ResolveReason = .match if self.context != context { resolveReason = .stale + Telemetry.shared.incrementStaleAccess() } return Evaluation( value: pathValue, diff --git a/Sources/Confidence/Http/HttpClient.swift b/Sources/Confidence/Http/HttpClient.swift index e6a800a4..cdb438c6 100644 --- a/Sources/Confidence/Http/HttpClient.swift +++ b/Sources/Confidence/Http/HttpClient.swift @@ -4,6 +4,7 @@ typealias HttpClientResult = Result, Error> internal protocol HttpClient { func post(path: String, data: Encodable) async throws -> HttpClientResult + func post(path: String, data: Encodable, header: Encodable) async throws -> HttpClientResult } struct HttpClientResponse { diff --git a/Sources/Confidence/Http/NetworkClient.swift b/Sources/Confidence/Http/NetworkClient.swift index 92f71e46..ee29c409 100644 --- a/Sources/Confidence/Http/NetworkClient.swift +++ b/Sources/Confidence/Http/NetworkClient.swift @@ -29,11 +29,22 @@ final class NetworkClient: HttpClient { self.timeoutIntervalForRequests = timeoutIntervalForRequests } + func post(path: String, data: any Encodable, header: any Encodable) async throws -> HttpClientResult where T : Decodable { + let request = try buildRequest(path: path, data: data, header: header) + return try await post(request: request) + } + public func post( path: String, data: Encodable ) async throws -> HttpClientResult { let request = try buildRequest(path: path, data: data) + return try await post(request: request) + } + + private func post( + request: URLRequest + ) async throws -> HttpClientResult { let requestResult = await perform(request: request, retry: self.retry) if let error = requestResult.error { return .failure(error) @@ -96,7 +107,7 @@ extension NetworkClient { return URL(string: "\(normalisedBase)\(normalisedPath)") } - private func buildRequest(path: String, data: Encodable) throws -> URLRequest { + private func buildRequest(path: String, data: Encodable, header: Encodable? = nil) throws -> URLRequest { guard let url = constructURL(base: baseUrl, path: path) else { throw ConfidenceError.internalError(message: "Could not create service url") } @@ -107,9 +118,20 @@ extension NetworkClient { request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept") + let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 + if let header = header { + let jsonHeaderData = try encoder.encode(header) + + if let headerJsonString = String(data: jsonHeaderData, encoding: .utf8) { + request.addValue(headerJsonString, forHTTPHeaderField: "Confidence-Metadata") + } + } + // TMP - TESTING + print(">> \(request.allHTTPHeaderFields)") + let jsonData = try encoder.encode(data) request.httpBody = jsonData diff --git a/Sources/Confidence/RemoteConfidenceClient.swift b/Sources/Confidence/RemoteConfidenceClient.swift index 7666f6ff..0e3a9ccb 100644 --- a/Sources/Confidence/RemoteConfidenceClient.swift +++ b/Sources/Confidence/RemoteConfidenceClient.swift @@ -7,6 +7,7 @@ public class RemoteConfidenceClient: ConfidenceClient { private var httpClient: HttpClient private var baseUrl: String private let debugLogger: DebugLogger? + private let telemetry = Telemetry.shared init( options: ConfidenceClientOptions, @@ -44,9 +45,10 @@ public class RemoteConfidenceClient: ConfidenceClient { sendTime: timeString, sdk: Sdk(id: metadata.name, version: metadata.version) ) + let header = telemetry.getSnapshot() do { let result: HttpClientResult = - try await self.httpClient.post(path: ":publish", data: request) + try await self.httpClient.post(path: ":publish", data: request, header: header) switch result { case .success(let successData): let status = successData.response.statusCode diff --git a/Sources/Confidence/RemoteResolveConfidenceClient.swift b/Sources/Confidence/RemoteResolveConfidenceClient.swift index 479dead8..dd133bec 100644 --- a/Sources/Confidence/RemoteResolveConfidenceClient.swift +++ b/Sources/Confidence/RemoteResolveConfidenceClient.swift @@ -4,6 +4,7 @@ class RemoteConfidenceResolveClient: ConfidenceResolveClient { private let targetingKey = "targeting_key" private var options: ConfidenceClientOptions private let metadata: ConfidenceMetadata + private let telemetry = Telemetry.shared private var httpClient: HttpClient private var applyOnResolve: Bool @@ -33,10 +34,10 @@ class RemoteConfidenceResolveClient: ConfidenceResolveClient { apply: applyOnResolve, sdk: Sdk(id: metadata.name, version: metadata.version) ) - + let header = telemetry.getSnapshot() do { let result: HttpClientResult = - try await self.httpClient.post(path: ":resolve", data: request) + try await self.httpClient.post(path: ":resolve", data: request, header: header) switch result { case .success(let successData): guard successData.response.status == .ok else { diff --git a/Sources/Confidence/Telemetry/TelemetryManager.swift b/Sources/Confidence/Telemetry/TelemetryManager.swift new file mode 100644 index 00000000..49a223d5 --- /dev/null +++ b/Sources/Confidence/Telemetry/TelemetryManager.swift @@ -0,0 +1,35 @@ +import Foundation + +protocol TelemetryManager { + func incrementStaleAccess() + func getSnapshot() -> TelemetryPayload +} + +class Telemetry: TelemetryManager { + private let queue = DispatchQueue(label: "com.confidence.telemetry_manager") + private var staleAccessCounter = 0; + + public init() {} + + static public let shared: TelemetryManager = Telemetry() + + public func getSnapshot() -> TelemetryPayload { + return queue.sync { + TelemetryPayload(staleAccess: getStaleAccessAndReset()) + } + } + + public func incrementStaleAccess() { + queue.sync { + staleAccessCounter += 1 + } + } + + private func getStaleAccessAndReset() -> Int { + return queue.sync { + let currentCounter = staleAccessCounter + staleAccessCounter = 0; + return currentCounter + } + } +} diff --git a/Sources/Confidence/Telemetry/TelemetryPayload.swift b/Sources/Confidence/Telemetry/TelemetryPayload.swift new file mode 100644 index 00000000..66b6c774 --- /dev/null +++ b/Sources/Confidence/Telemetry/TelemetryPayload.swift @@ -0,0 +1,5 @@ +import Foundation + +struct TelemetryPayload: Encodable { + var staleAccess = 0 +} diff --git a/Tests/ConfidenceTests/Helpers/HttpClientMock.swift b/Tests/ConfidenceTests/Helpers/HttpClientMock.swift index 2eead808..81bd326c 100644 --- a/Tests/ConfidenceTests/Helpers/HttpClientMock.swift +++ b/Tests/ConfidenceTests/Helpers/HttpClientMock.swift @@ -23,6 +23,10 @@ final class HttpClientMock: HttpClient { try handlePost(path: path, data: data) } + func post(path: String, data: any Encodable, header: any Encodable) async throws -> HttpClientResult where T : Decodable { + try handlePost(path: path, data: data) + } + private func handlePost( path: String, data: Encodable ) throws -> HttpClientResult where T: Decodable {