From 8da7901776e9b3d0c502f6dc1e91cb6843af2724 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Wed, 26 Mar 2025 16:41:19 +0400 Subject: [PATCH 1/4] feat: add enrichment of StartRequest with OS, device ID, version --- Coder-Desktop/VPN/Manager.swift | 3 + Coder-Desktop/VPNLib/TelemetryEnricher.swift | 31 ++ Coder-Desktop/VPNLib/vpn.pb.swift | 492 ++++++++++++++++++ Coder-Desktop/VPNLib/vpn.proto | 49 ++ .../VPNLibTests/TelemetryEnricherTests.swift | 27 + 5 files changed, 602 insertions(+) create mode 100644 Coder-Desktop/VPNLib/TelemetryEnricher.swift create mode 100644 Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift diff --git a/Coder-Desktop/VPN/Manager.swift b/Coder-Desktop/VPN/Manager.swift index a1dc6bc0..adff1434 100644 --- a/Coder-Desktop/VPN/Manager.swift +++ b/Coder-Desktop/VPN/Manager.swift @@ -6,6 +6,7 @@ import VPNLib actor Manager { let ptp: PacketTunnelProvider let cfg: ManagerConfig + let telemetryEnricher: TelemetryEnricher let tunnelHandle: TunnelHandle let speaker: Speaker @@ -19,6 +20,7 @@ actor Manager { init(with: PacketTunnelProvider, cfg: ManagerConfig) async throws(ManagerError) { ptp = with self.cfg = cfg + telemetryEnricher = TelemetryEnricher() #if arch(arm64) let dylibPath = cfg.serverUrl.appending(path: "bin/coder-vpn-darwin-arm64.dylib") #elseif arch(x86_64) @@ -176,6 +178,7 @@ actor Manager { req.value = header.value } } + req = telemetryEnricher.enrich(req) } }) } catch { diff --git a/Coder-Desktop/VPNLib/TelemetryEnricher.swift b/Coder-Desktop/VPNLib/TelemetryEnricher.swift new file mode 100644 index 00000000..a724e055 --- /dev/null +++ b/Coder-Desktop/VPNLib/TelemetryEnricher.swift @@ -0,0 +1,31 @@ +import Foundation + +public struct TelemetryEnricher { + private let deviceID: String + private let version: String? + + public init() { + version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + + let userDefaults = UserDefaults.standard + let key = "deviceID" + + if let existingID = userDefaults.string(forKey: key) { + deviceID = existingID + } else { + let newID = UUID().uuidString + userDefaults.set(newID, forKey: key) + deviceID = newID + } + } + + public func enrich(_ original: Vpn_StartRequest) -> Vpn_StartRequest { + var req = original + req.deviceOs = "macOS" + req.deviceID = deviceID + if version != nil { + req.coderDesktopVersion = version! + } + return req + } +} diff --git a/Coder-Desktop/VPNLib/vpn.pb.swift b/Coder-Desktop/VPNLib/vpn.pb.swift index 525f55bb..3e728045 100644 --- a/Coder-Desktop/VPNLib/vpn.pb.swift +++ b/Coder-Desktop/VPNLib/vpn.pb.swift @@ -175,6 +175,118 @@ public struct Vpn_TunnelMessage: Sendable { fileprivate var _rpc: Vpn_RPC? = nil } +/// ClientMessage is a message from the client (to the service). Windows only. +public struct Vpn_ClientMessage: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var rpc: Vpn_RPC { + get {return _rpc ?? Vpn_RPC()} + set {_rpc = newValue} + } + /// Returns true if `rpc` has been explicitly set. + public var hasRpc: Bool {return self._rpc != nil} + /// Clears the value of `rpc`. Subsequent reads from it will return its default value. + public mutating func clearRpc() {self._rpc = nil} + + public var msg: Vpn_ClientMessage.OneOf_Msg? = nil + + public var start: Vpn_StartRequest { + get { + if case .start(let v)? = msg {return v} + return Vpn_StartRequest() + } + set {msg = .start(newValue)} + } + + public var stop: Vpn_StopRequest { + get { + if case .stop(let v)? = msg {return v} + return Vpn_StopRequest() + } + set {msg = .stop(newValue)} + } + + public var status: Vpn_StatusRequest { + get { + if case .status(let v)? = msg {return v} + return Vpn_StatusRequest() + } + set {msg = .status(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_Msg: Equatable, Sendable { + case start(Vpn_StartRequest) + case stop(Vpn_StopRequest) + case status(Vpn_StatusRequest) + + } + + public init() {} + + fileprivate var _rpc: Vpn_RPC? = nil +} + +/// ServiceMessage is a message from the service (to the client). Windows only. +public struct Vpn_ServiceMessage: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var rpc: Vpn_RPC { + get {return _rpc ?? Vpn_RPC()} + set {_rpc = newValue} + } + /// Returns true if `rpc` has been explicitly set. + public var hasRpc: Bool {return self._rpc != nil} + /// Clears the value of `rpc`. Subsequent reads from it will return its default value. + public mutating func clearRpc() {self._rpc = nil} + + public var msg: Vpn_ServiceMessage.OneOf_Msg? = nil + + public var start: Vpn_StartResponse { + get { + if case .start(let v)? = msg {return v} + return Vpn_StartResponse() + } + set {msg = .start(newValue)} + } + + public var stop: Vpn_StopResponse { + get { + if case .stop(let v)? = msg {return v} + return Vpn_StopResponse() + } + set {msg = .stop(newValue)} + } + + /// either in reply to a StatusRequest or broadcasted + public var status: Vpn_Status { + get { + if case .status(let v)? = msg {return v} + return Vpn_Status() + } + set {msg = .status(newValue)} + } + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum OneOf_Msg: Equatable, Sendable { + case start(Vpn_StartResponse) + case stop(Vpn_StopResponse) + /// either in reply to a StatusRequest or broadcasted + case status(Vpn_Status) + + } + + public init() {} + + fileprivate var _rpc: Vpn_RPC? = nil +} + /// Log is a log message generated by the tunnel. The manager should log it to the system log. It is /// one-way tunnel -> manager with no response. public struct Vpn_Log: Sendable { @@ -599,6 +711,15 @@ public struct Vpn_StartRequest: Sendable { public var headers: [Vpn_StartRequest.Header] = [] + /// Device ID from Coder Desktop + public var deviceID: String = String() + + /// Device OS from Coder Desktop + public var deviceOs: String = String() + + /// Coder Desktop version + public var coderDesktopVersion: String = String() + public var unknownFields = SwiftProtobuf.UnknownStorage() /// Additional HTTP headers added to all requests @@ -661,6 +782,94 @@ public struct Vpn_StopResponse: Sendable { public init() {} } +/// StatusRequest is a request to get the status of the tunnel. The manager +/// replies with a Status. +public struct Vpn_StatusRequest: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public init() {} +} + +/// Status is sent in response to a StatusRequest or broadcasted to all clients +/// when the status changes. +public struct Vpn_Status: Sendable { + // SwiftProtobuf.Message conformance is added in an extension below. See the + // `Message` and `Message+*Additions` files in the SwiftProtobuf library for + // methods supported on all messages. + + public var lifecycle: Vpn_Status.Lifecycle = .unknown + + public var errorMessage: String = String() + + /// This will be a FULL update with all workspaces and agents, so clients + /// should replace their current peer state. Only the Upserted fields will + /// be populated. + public var peerUpdate: Vpn_PeerUpdate { + get {return _peerUpdate ?? Vpn_PeerUpdate()} + set {_peerUpdate = newValue} + } + /// Returns true if `peerUpdate` has been explicitly set. + public var hasPeerUpdate: Bool {return self._peerUpdate != nil} + /// Clears the value of `peerUpdate`. Subsequent reads from it will return its default value. + public mutating func clearPeerUpdate() {self._peerUpdate = nil} + + public var unknownFields = SwiftProtobuf.UnknownStorage() + + public enum Lifecycle: SwiftProtobuf.Enum, Swift.CaseIterable { + public typealias RawValue = Int + case unknown // = 0 + case starting // = 1 + case started // = 2 + case stopping // = 3 + case stopped // = 4 + case UNRECOGNIZED(Int) + + public init() { + self = .unknown + } + + public init?(rawValue: Int) { + switch rawValue { + case 0: self = .unknown + case 1: self = .starting + case 2: self = .started + case 3: self = .stopping + case 4: self = .stopped + default: self = .UNRECOGNIZED(rawValue) + } + } + + public var rawValue: Int { + switch self { + case .unknown: return 0 + case .starting: return 1 + case .started: return 2 + case .stopping: return 3 + case .stopped: return 4 + case .UNRECOGNIZED(let i): return i + } + } + + // The compiler won't synthesize support with the UNRECOGNIZED case. + public static let allCases: [Vpn_Status.Lifecycle] = [ + .unknown, + .starting, + .started, + .stopping, + .stopped, + ] + + } + + public init() {} + + fileprivate var _peerUpdate: Vpn_PeerUpdate? = nil +} + // MARK: - Code below here is support for the SwiftProtobuf runtime. fileprivate let _protobuf_package = "vpn" @@ -945,6 +1154,194 @@ extension Vpn_TunnelMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem } } +extension Vpn_ClientMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ClientMessage" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "rpc"), + 2: .same(proto: "start"), + 3: .same(proto: "stop"), + 4: .same(proto: "status"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._rpc) }() + case 2: try { + var v: Vpn_StartRequest? + var hadOneofValue = false + if let current = self.msg { + hadOneofValue = true + if case .start(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.msg = .start(v) + } + }() + case 3: try { + var v: Vpn_StopRequest? + var hadOneofValue = false + if let current = self.msg { + hadOneofValue = true + if case .stop(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.msg = .stop(v) + } + }() + case 4: try { + var v: Vpn_StatusRequest? + var hadOneofValue = false + if let current = self.msg { + hadOneofValue = true + if case .status(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.msg = .status(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._rpc { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + switch self.msg { + case .start?: try { + guard case .start(let v)? = self.msg else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .stop?: try { + guard case .stop(let v)? = self.msg else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .status?: try { + guard case .status(let v)? = self.msg else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Vpn_ClientMessage, rhs: Vpn_ClientMessage) -> Bool { + if lhs._rpc != rhs._rpc {return false} + if lhs.msg != rhs.msg {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Vpn_ServiceMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".ServiceMessage" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "rpc"), + 2: .same(proto: "start"), + 3: .same(proto: "stop"), + 4: .same(proto: "status"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularMessageField(value: &self._rpc) }() + case 2: try { + var v: Vpn_StartResponse? + var hadOneofValue = false + if let current = self.msg { + hadOneofValue = true + if case .start(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.msg = .start(v) + } + }() + case 3: try { + var v: Vpn_StopResponse? + var hadOneofValue = false + if let current = self.msg { + hadOneofValue = true + if case .stop(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.msg = .stop(v) + } + }() + case 4: try { + var v: Vpn_Status? + var hadOneofValue = false + if let current = self.msg { + hadOneofValue = true + if case .status(let m) = current {v = m} + } + try decoder.decodeSingularMessageField(value: &v) + if let v = v { + if hadOneofValue {try decoder.handleConflictingOneOf()} + self.msg = .status(v) + } + }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + try { if let v = self._rpc { + try visitor.visitSingularMessageField(value: v, fieldNumber: 1) + } }() + switch self.msg { + case .start?: try { + guard case .start(let v)? = self.msg else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + }() + case .stop?: try { + guard case .stop(let v)? = self.msg else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + }() + case .status?: try { + guard case .status(let v)? = self.msg else { preconditionFailure() } + try visitor.visitSingularMessageField(value: v, fieldNumber: 4) + }() + case nil: break + } + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Vpn_ServiceMessage, rhs: Vpn_ServiceMessage) -> Bool { + if lhs._rpc != rhs._rpc {return false} + if lhs.msg != rhs.msg {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + extension Vpn_Log: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { public static let protoMessageName: String = _protobuf_package + ".Log" public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ @@ -1650,6 +2047,9 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme 2: .standard(proto: "coder_url"), 3: .standard(proto: "api_token"), 4: .same(proto: "headers"), + 5: .standard(proto: "device_id"), + 6: .standard(proto: "device_os"), + 7: .standard(proto: "coder_desktop_version"), ] public mutating func decodeMessage(decoder: inout D) throws { @@ -1662,6 +2062,9 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme case 2: try { try decoder.decodeSingularStringField(value: &self.coderURL) }() case 3: try { try decoder.decodeSingularStringField(value: &self.apiToken) }() case 4: try { try decoder.decodeRepeatedMessageField(value: &self.headers) }() + case 5: try { try decoder.decodeSingularStringField(value: &self.deviceID) }() + case 6: try { try decoder.decodeSingularStringField(value: &self.deviceOs) }() + case 7: try { try decoder.decodeSingularStringField(value: &self.coderDesktopVersion) }() default: break } } @@ -1680,6 +2083,15 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme if !self.headers.isEmpty { try visitor.visitRepeatedMessageField(value: self.headers, fieldNumber: 4) } + if !self.deviceID.isEmpty { + try visitor.visitSingularStringField(value: self.deviceID, fieldNumber: 5) + } + if !self.deviceOs.isEmpty { + try visitor.visitSingularStringField(value: self.deviceOs, fieldNumber: 6) + } + if !self.coderDesktopVersion.isEmpty { + try visitor.visitSingularStringField(value: self.coderDesktopVersion, fieldNumber: 7) + } try unknownFields.traverse(visitor: &visitor) } @@ -1688,6 +2100,9 @@ extension Vpn_StartRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme if lhs.coderURL != rhs.coderURL {return false} if lhs.apiToken != rhs.apiToken {return false} if lhs.headers != rhs.headers {return false} + if lhs.deviceID != rhs.deviceID {return false} + if lhs.deviceOs != rhs.deviceOs {return false} + if lhs.coderDesktopVersion != rhs.coderDesktopVersion {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1825,3 +2240,80 @@ extension Vpn_StopResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImpleme return true } } + +extension Vpn_StatusRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".StatusRequest" + public static let _protobuf_nameMap = SwiftProtobuf._NameMap() + + public mutating func decodeMessage(decoder: inout D) throws { + // Load everything into unknown fields + while try decoder.nextFieldNumber() != nil {} + } + + public func traverse(visitor: inout V) throws { + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Vpn_StatusRequest, rhs: Vpn_StatusRequest) -> Bool { + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Vpn_Status: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { + public static let protoMessageName: String = _protobuf_package + ".Status" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 1: .same(proto: "lifecycle"), + 2: .standard(proto: "error_message"), + 3: .standard(proto: "peer_update"), + ] + + public mutating func decodeMessage(decoder: inout D) throws { + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeSingularEnumField(value: &self.lifecycle) }() + case 2: try { try decoder.decodeSingularStringField(value: &self.errorMessage) }() + case 3: try { try decoder.decodeSingularMessageField(value: &self._peerUpdate) }() + default: break + } + } + } + + public func traverse(visitor: inout V) throws { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if self.lifecycle != .unknown { + try visitor.visitSingularEnumField(value: self.lifecycle, fieldNumber: 1) + } + if !self.errorMessage.isEmpty { + try visitor.visitSingularStringField(value: self.errorMessage, fieldNumber: 2) + } + try { if let v = self._peerUpdate { + try visitor.visitSingularMessageField(value: v, fieldNumber: 3) + } }() + try unknownFields.traverse(visitor: &visitor) + } + + public static func ==(lhs: Vpn_Status, rhs: Vpn_Status) -> Bool { + if lhs.lifecycle != rhs.lifecycle {return false} + if lhs.errorMessage != rhs.errorMessage {return false} + if lhs._peerUpdate != rhs._peerUpdate {return false} + if lhs.unknownFields != rhs.unknownFields {return false} + return true + } +} + +extension Vpn_Status.Lifecycle: SwiftProtobuf._ProtoNameProviding { + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + 0: .same(proto: "UNKNOWN"), + 1: .same(proto: "STARTING"), + 2: .same(proto: "STARTED"), + 3: .same(proto: "STOPPING"), + 4: .same(proto: "STOPPED"), + ] +} diff --git a/Coder-Desktop/VPNLib/vpn.proto b/Coder-Desktop/VPNLib/vpn.proto index 9d9c2435..b3fe54c5 100644 --- a/Coder-Desktop/VPNLib/vpn.proto +++ b/Coder-Desktop/VPNLib/vpn.proto @@ -44,6 +44,26 @@ message TunnelMessage { } } +// ClientMessage is a message from the client (to the service). Windows only. +message ClientMessage { + RPC rpc = 1; + oneof msg { + StartRequest start = 2; + StopRequest stop = 3; + StatusRequest status = 4; + } +} + +// ServiceMessage is a message from the service (to the client). Windows only. +message ServiceMessage { + RPC rpc = 1; + oneof msg { + StartResponse start = 2; + StopResponse stop = 3; + Status status = 4; // either in reply to a StatusRequest or broadcasted + } +} + // Log is a log message generated by the tunnel. The manager should log it to the system log. It is // one-way tunnel -> manager with no response. message Log { @@ -185,6 +205,12 @@ message StartRequest { string value = 2; } repeated Header headers = 4; + // Device ID from Coder Desktop + string device_id = 5; + // Device OS from Coder Desktop + string device_os = 6; + // Coder Desktop version + string coder_desktop_version = 7; } message StartResponse { @@ -202,3 +228,26 @@ message StopResponse { bool success = 1; string error_message = 2; } + +// StatusRequest is a request to get the status of the tunnel. The manager +// replies with a Status. +message StatusRequest {} + +// Status is sent in response to a StatusRequest or broadcasted to all clients +// when the status changes. +message Status { + enum Lifecycle { + UNKNOWN = 0; + STARTING = 1; + STARTED = 2; + STOPPING = 3; + STOPPED = 4; + } + Lifecycle lifecycle = 1; + string error_message = 2; + + // This will be a FULL update with all workspaces and agents, so clients + // should replace their current peer state. Only the Upserted fields will + // be populated. + PeerUpdate peer_update = 3; +} diff --git a/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift new file mode 100644 index 00000000..0bd56753 --- /dev/null +++ b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift @@ -0,0 +1,27 @@ +import Testing +@testable import VPNLib + +@Suite(.timeLimit(.minutes(1))) +struct TelemetryEnricherTests { + + @Test func testEnrichStartRequest() throws { + let enricher0 = TelemetryEnricher() + let original = Vpn_StartRequest.with { req in + req.coderURL = "https://example.com" + req.tunnelFileDescriptor = 123 + } + var enriched = enricher0.enrich(original) + #expect(enriched.coderURL == "https://example.com") + #expect(enriched.tunnelFileDescriptor == 123) + #expect(enriched.deviceOs == "macOS") + #expect(enriched.coderDesktopVersion.contains(try Regex(#"^\d+\.\d+\.\d+$"#))) + let deviceID = enriched.deviceID + #expect(!deviceID.isEmpty) + + // check we get the same deviceID from a new enricher + let enricher1 = TelemetryEnricher() + enriched = enricher1.enrich(original) + #expect(enriched.deviceID == deviceID) + } + +} From fdcf9c3d4341e313a19c5e2b62b7225b25f75206 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Wed, 26 Mar 2025 16:51:50 +0400 Subject: [PATCH 2/4] fix linting --- Coder-Desktop/VPNLib/TelemetryEnricher.swift | 8 ++++---- Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Coder-Desktop/VPNLib/TelemetryEnricher.swift b/Coder-Desktop/VPNLib/TelemetryEnricher.swift index a724e055..c112a6a1 100644 --- a/Coder-Desktop/VPNLib/TelemetryEnricher.swift +++ b/Coder-Desktop/VPNLib/TelemetryEnricher.swift @@ -3,13 +3,13 @@ import Foundation public struct TelemetryEnricher { private let deviceID: String private let version: String? - + public init() { version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - + let userDefaults = UserDefaults.standard let key = "deviceID" - + if let existingID = userDefaults.string(forKey: key) { deviceID = existingID } else { @@ -18,7 +18,7 @@ public struct TelemetryEnricher { deviceID = newID } } - + public func enrich(_ original: Vpn_StartRequest) -> Vpn_StartRequest { var req = original req.deviceOs = "macOS" diff --git a/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift index 0bd56753..9f3173f3 100644 --- a/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift +++ b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift @@ -3,7 +3,6 @@ import Testing @Suite(.timeLimit(.minutes(1))) struct TelemetryEnricherTests { - @Test func testEnrichStartRequest() throws { let enricher0 = TelemetryEnricher() let original = Vpn_StartRequest.with { req in @@ -14,7 +13,7 @@ struct TelemetryEnricherTests { #expect(enriched.coderURL == "https://example.com") #expect(enriched.tunnelFileDescriptor == 123) #expect(enriched.deviceOs == "macOS") - #expect(enriched.coderDesktopVersion.contains(try Regex(#"^\d+\.\d+\.\d+$"#))) + #expect(try enriched.coderDesktopVersion.contains(Regex(#"^\d+\.\d+\.\d+$"#))) let deviceID = enriched.deviceID #expect(!deviceID.isEmpty) @@ -23,5 +22,4 @@ struct TelemetryEnricherTests { enriched = enricher1.enrich(original) #expect(enriched.deviceID == deviceID) } - } From 9da59a0832c3572efa45ad9471100010ea2515a6 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 27 Mar 2025 10:29:51 +0400 Subject: [PATCH 3/4] update VPN protocol version; fmt & lint --- Coder-Desktop/VPNLib/Speaker.swift | 6 ++++-- Coder-Desktop/VPNLib/TelemetryEnricher.swift | 4 ++-- Coder-Desktop/VPNLibTests/SpeakerTests.swift | 2 +- Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Coder-Desktop/VPNLib/Speaker.swift b/Coder-Desktop/VPNLib/Speaker.swift index b53f50a8..c1f21e77 100644 --- a/Coder-Desktop/VPNLib/Speaker.swift +++ b/Coder-Desktop/VPNLib/Speaker.swift @@ -89,7 +89,9 @@ public actor Speaker Date: Thu, 27 Mar 2025 10:47:56 +0400 Subject: [PATCH 4/4] fix tests --- Coder-Desktop/VPNLib/Speaker.swift | 5 +++-- Coder-Desktop/VPNLibTests/SpeakerTests.swift | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Coder-Desktop/VPNLib/Speaker.swift b/Coder-Desktop/VPNLib/Speaker.swift index c1f21e77..88e46b05 100644 --- a/Coder-Desktop/VPNLib/Speaker.swift +++ b/Coder-Desktop/VPNLib/Speaker.swift @@ -88,7 +88,8 @@ public actor Speaker pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy