Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: poc: Telemetry architecture #169

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct ConfidenceDemoApp: App {
WindowGroup {
let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? ""
let confidence = Confidence.Builder(clientSecret: secret, loggerLevel: .TRACE)
.withContext(initialContext: ["targeting_key": ConfidenceValue(string: UUID.init().uuidString)])
// .withContext(initialContext: ["targeting_key": ConfidenceValue(string: UUID.init().uuidString)])
.build()

let status = Status()
Expand All @@ -42,6 +42,6 @@ struct ConfidenceDemoApp: App {

extension ConfidenceDemoApp {
func setup(confidence: Confidence) async throws {
try await confidence.fetchAndActivate()
// try await confidence.fetchAndActivate()
}
}
76 changes: 74 additions & 2 deletions ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ struct ContentView: View {
@ObservedObject var status: Status
@StateObject var text = DisplayText()
@StateObject var color = FlagColor()
@State private var user = "logged out"
@State private var logginIn = false
@State private var evaluationReason = "Last Reason: ---"
@State private var evaluationError = "Last Error: ---"

private let confidence: Confidence

Expand All @@ -17,24 +21,88 @@ struct ContentView: View {
var body: some View {
if case .ready = status.state {
VStack {
Text("Current user:")
if (logginIn) {
ProgressView()
} else {
Text("\(user)")
.font(.title)
.bold()
}
Spacer()
Image(systemName: "flag")
.imageScale(.large)
.font(.title)
.foregroundColor(color.color)
.padding(10)
Text(text.text)
Spacer()
Button("Login yellow user") {
confidence.putContext(key: "user_id", value: ConfidenceValue.init(string: "user1"))
Task {
logginIn = true
try await confidence.fetchAndActivate()
user = "yellow_user"
logginIn = false
}
}
Button("Login green user") {
confidence.putContext(key: "user_id", value: ConfidenceValue.init(string: "user2"))
Task {
logginIn = true
try await confidence.fetchAndActivate()
logginIn = false
user = "green_user"
}
}
.padding(.bottom)
Button("Get remote flag value") {
text.text = confidence.getValue(key: "swift-demoapp.color", defaultValue: "ERROR")
let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: "DefaultValue")
evaluationReason = "Last Reason: \(eval.reason)"
evaluationError = "Last Error: \(eval.errorCode?.description ?? .none ?? "None")"
text.text = eval.value
if text.text == "Green" {
color.color = .green
} else if text.text == "Yellow" {
color.color = .yellow
} else {
color.color = .red
color.color = .gray
}
}
VStack(alignment: .leading) {
HStack {
Text(evaluationReason)
.padding(.horizontal)
Spacer()
}
HStack {
Text(evaluationError)
.padding(.horizontal)
Spacer()
}
}
.padding(.bottom)
Button("Get remote flag value with TypeMismatch ⚠️") {
let eval = confidence.getEvaluation(key: "swift-demoapp.color", defaultValue: true)
evaluationReason = "Last Reason: \(eval.reason)"
evaluationError = "Last Error: \(eval.errorCode?.description ?? .none ?? "None")"
if text.text == "Green" {
color.color = .green
} else if text.text == "Yellow" {
color.color = .yellow
} else {
color.color = .gray
}
}
.padding(.bottom)
Button("Generate event") {
try! confidence.track(eventName: "Test", data: [:])
}
.padding(.top)
Button("Flush 🚽") {
confidence.flush()
}
.padding(.bottom)
}
.padding()
} else if case .error(let error) = status.state {
Expand All @@ -51,6 +119,10 @@ struct ContentView: View {
}
}

class Model: ObservableObject {

}

class DisplayText: ObservableObject {
@Published var text = "Hello World!"
}
Expand Down
9 changes: 9 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
{
"object": {
"pins": [
{
"package": "SwiftProtobuf",
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
"revision": "ebc7251dd5b37f627c93698e4374084d98409633",
"version": "1.28.2"
}
},
{
"package": "OpenFeature",
"repositoryURL": "[email protected]:open-feature/swift-sdk.git",
Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ let package = Package(
],
dependencies: [
.package(url: "[email protected]:open-feature/swift-sdk.git", from: "0.1.0"),
.package(url: "https://github.com/apple/swift-protobuf.git", from: "1.0.0"),
],
targets: [
.target(
name: "Confidence",
dependencies: [],
dependencies: [
.product(name: "SwiftProtobuf", package: "swift-protobuf")
],
plugins: []
),
.target(
Expand Down
4 changes: 3 additions & 1 deletion Sources/Confidence/Apply/FlagApplierWithRetries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -139,7 +140,8 @@ final class FlagApplierWithRetries: FlagApplier {
request: ApplyFlagsRequest
) async -> ApplyFlagResult {
do {
return try await httpClient.post(path: ":apply", data: request)
let header: Data = telemetry.getSnapshot()
return try await httpClient.post(path: ":apply", data: request, header: header)
} catch {
return .failure(handleError(error: error))
}
Expand Down
6 changes: 1 addition & 5 deletions Sources/Confidence/Confidence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ public class Confidence: ConfidenceEventSender {
internal let remoteFlagResolver: ConfidenceResolveClient
internal let contextReconciliatedChanges = PassthroughSubject<String, Never>()

public static let sdkId: String = "SDK_ID_SWIFT_CONFIDENCE"

required init(
clientSecret: String,
region: ConfidenceRegion,
Expand Down Expand Up @@ -398,9 +396,7 @@ extension Confidence {
credentials: ConfidenceClientCredentials.clientSecret(secret: clientSecret),
region: region,
timeoutIntervalForRequest: timeout)
let metadata = ConfidenceMetadata(
name: sdkId,
version: "1.0.1") // x-release-please-version
let metadata = ConfidenceMetadata.defaultMetadata
let uploader = RemoteConfidenceClient(
options: options,
metadata: metadata,
Expand Down
12 changes: 8 additions & 4 deletions Sources/Confidence/ConfidenceMetadata.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import Foundation

struct ConfidenceMetadata {
private static let sdkName: String = "SDK_ID_SWIFT_CONFIDENCE"
private static let sdkId: Int = 13 // TODO enstablish cross-language identifiers

public var id: Int
public var name: String
public var version: String

public init(name: String, version: String) {
self.name = name
self.version = version
}
public static let defaultMetadata = ConfidenceMetadata(
id: sdkId,
name: sdkName,
version: "1.0.1") // x-release-please-version
}
19 changes: 18 additions & 1 deletion Sources/Confidence/FlagEvaluation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,27 @@ public struct Evaluation<T> {
public let errorMessage: String?
}

public enum ErrorCode {
public enum ErrorCode: CustomStringConvertible {
case providerNotReady
case invalidContext
case flagNotFound
case evaluationError
case typeMismatch

public var description: String {
switch self {
case .providerNotReady:
return "Provider is not ready."
case .invalidContext:
return "Invalid context."
case .flagNotFound:
return "Flag not found."
case .evaluationError:
return "Evaluation error occurred."
case .typeMismatch:
return "Type mismatch encountered."
}
}
}

struct FlagResolution: Encodable, Decodable, Equatable {
Expand Down Expand Up @@ -75,6 +90,7 @@ extension FlagResolution {
var resolveReason: ResolveReason = .match
if self.context != context {
resolveReason = .stale
Telemetry.shared.incrementStaleAccess()
}
if let typedValue = typedValue {
return Evaluation(
Expand All @@ -95,6 +111,7 @@ extension FlagResolution {
errorMessage: nil
)
} else {
Telemetry.shared.incrementFlagTypeMismatch()
return Evaluation(
value: defaultValue,
variant: nil,
Expand Down
1 change: 1 addition & 0 deletions Sources/Confidence/Http/HttpClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ typealias HttpClientResult<T> = Result<HttpClientResponse<T>, Error>

internal protocol HttpClient {
func post<T: Decodable>(path: String, data: Encodable) async throws -> HttpClientResult<T>
func post<T: Decodable>(path: String, data: Encodable, header: Data) async throws -> HttpClientResult<T>
}

struct HttpClientResponse<T> {
Expand Down
21 changes: 20 additions & 1 deletion Sources/Confidence/Http/NetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,22 @@
self.timeoutIntervalForRequests = timeoutIntervalForRequests
}

func post<T>(path: String, data: any Encodable, header: Data) async throws -> HttpClientResult<T> where T : Decodable {

Check failure on line 32 in Sources/Confidence/Http/NetworkClient.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Colon Spacing Violation: Colons should be next to the identifier when specifying a type and next to the key in dictionary literals (colon)
let request = try buildRequest(path: path, data: data, header: header)
return try await post(request: request)
}

public func post<T: Decodable>(
path: String,
data: Encodable
) async throws -> HttpClientResult<T> {
let request = try buildRequest(path: path, data: data)
return try await post(request: request)
}

private func post<T: Decodable>(
request: URLRequest
) async throws -> HttpClientResult<T> {
let requestResult = await perform(request: request, retry: self.retry)
if let error = requestResult.error {
return .failure(error)
Expand Down Expand Up @@ -96,7 +107,7 @@
return URL(string: "\(normalisedBase)\(normalisedPath)")
}

private func buildRequest(path: String, data: Encodable) throws -> URLRequest {
private func buildRequest(path: String, data: Encodable, header: Data? = nil) throws -> URLRequest {
guard let url = constructURL(base: baseUrl, path: path) else {
throw ConfidenceError.internalError(message: "Could not create service url")
}
Expand All @@ -107,9 +118,17 @@
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")


let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601

if let header = header {
request.addValue(header.base64EncodedString(), forHTTPHeaderField: "Confidence-Metadata")
// TMP - TESTING
let telemetryData = try LibraryData(serializedBytes: header)
print(telemetryData)
}

let jsonData = try encoder.encode(data)
request.httpBody = jsonData

Expand Down
4 changes: 3 additions & 1 deletion Sources/Confidence/RemoteConfidenceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<PublishEventResponse> =
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
Expand Down
5 changes: 3 additions & 2 deletions Sources/Confidence/RemoteResolveConfidenceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<ResolveFlagsResponse> =
try await self.httpClient.post(path: ":resolve", data: request)
try await self.httpClient.post(path: ":resolve", data: request, header: header)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this header param in the post function accept?
IMO the header is pretty likely to be a key-value "tuple" that's gonna be appended to other headers.
If that's the case we probably want to have a common header key X-CONFIDENCE-TELEMETRY and pass the full telemetry data as the value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am concerned about using the header in general, as it might be handled differently by various proxies and might have different and more unpredictable size limitations. We would also create new code paths in the backend to deal with data in the header (minor point, but still worth mentioning in the tradeoff discussions)

switch result {
case .success(let successData):
guard successData.response.status == .ok else {
Expand Down
Loading
Loading