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/Speaker.swift b/Coder-Desktop/VPNLib/Speaker.swift index b53f50a8..88e46b05 100644 --- a/Coder-Desktop/VPNLib/Speaker.swift +++ b/Coder-Desktop/VPNLib/Speaker.swift @@ -88,8 +88,11 @@ public actor Speaker Vpn_StartRequest { + var req = original + req.deviceOs = "macOS" + req.deviceID = deviceID + if let version { + 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/SpeakerTests.swift b/Coder-Desktop/VPNLibTests/SpeakerTests.swift index fd8ffb76..dd837d70 100644 --- a/Coder-Desktop/VPNLibTests/SpeakerTests.swift +++ b/Coder-Desktop/VPNLibTests/SpeakerTests.swift @@ -29,14 +29,15 @@ struct SpeakerTests: Sendable { handshaker = Handshaker( writeFD: pipeMT.fileHandleForWriting, dispatch: dispatch, queue: queue, - role: .manager + role: .manager, + versions: [ProtoVersion(1, 1)] ) } @Test func handshake() async throws { async let v = handshaker.handshake() try await uut.handshake() - #expect(try await v == ProtoVersion(1, 0)) + #expect(try await v == ProtoVersion(1, 1)) } @Test func handleSingleMessage() async throws { diff --git a/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift new file mode 100644 index 00000000..becf6b37 --- /dev/null +++ b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift @@ -0,0 +1,25 @@ +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(try enriched.coderDesktopVersion.contains(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) + } +} 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