diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bb5893c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# EditorConfig is awesome: https://editorconfig.org +root = true + +[*] + +indent_style = space +tab_width = 8 +indent_size = 4 + +end_of_line = lf +insert_final_newline = true + +max_line_length = 160 +trim_trailing_whitespace = true diff --git a/Package.swift b/Package.swift index d63b041..0efe205 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version: 5.9 import PackageDescription let package = Package( @@ -19,15 +19,18 @@ let package = Package( .target( name: "TelemetryDeck", dependencies: ["TelemetryClient"], - resources: [.copy("PrivacyInfo.xcprivacy")] + resources: [.copy("PrivacyInfo.xcprivacy")], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] ), .target( name: "TelemetryClient", - resources: [.copy("PrivacyInfo.xcprivacy")] + resources: [.copy("PrivacyInfo.xcprivacy")], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] ), .testTarget( name: "TelemetryClientTests", - dependencies: ["TelemetryClient"] + dependencies: ["TelemetryClient"], + swiftSettings: [.enableExperimentalFeature("StrictConcurrency")] ) ] ) diff --git a/Sources/TelemetryClient/JSONFormatting.swift b/Sources/TelemetryClient/JSONFormatting.swift index d3ab40d..599a786 100644 --- a/Sources/TelemetryClient/JSONFormatting.swift +++ b/Sources/TelemetryClient/JSONFormatting.swift @@ -1,19 +1,19 @@ import Foundation extension Formatter { - static let iso8601: ISO8601DateFormatter = { + static var iso8601: ISO8601DateFormatter { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter - }() + } - static let iso8601noFS = ISO8601DateFormatter() + static var iso8601noFS: ISO8601DateFormatter { ISO8601DateFormatter() } - static let iso8601dateOnly: ISO8601DateFormatter = { + static var iso8601dateOnly: ISO8601DateFormatter { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate] return formatter - }() + } } extension JSONDecoder.DateDecodingStrategy { @@ -28,7 +28,7 @@ extension JSONDecoder.DateDecodingStrategy { } extension JSONDecoder { - static var telemetryDecoder: JSONDecoder = { + static var telemetryDecoder: JSONDecoder { let decoder = JSONDecoder() let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" @@ -36,17 +36,17 @@ extension JSONDecoder { dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) decoder.dateDecodingStrategy = .formatted(dateFormatter) return decoder - }() + } - static var druidDecoder: JSONDecoder = { + static var druidDecoder: JSONDecoder { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .customISO8601 return decoder - }() + } } extension JSONEncoder { - static var telemetryEncoder: JSONEncoder = { + static var telemetryEncoder: JSONEncoder { let encoder = JSONEncoder() let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" @@ -54,7 +54,7 @@ extension JSONEncoder { dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) encoder.dateEncodingStrategy = .formatted(dateFormatter) return encoder - }() + } } extension Data { diff --git a/Sources/TelemetryClient/LogHandler.swift b/Sources/TelemetryClient/LogHandler.swift index 7f9244f..73695bd 100644 --- a/Sources/TelemetryClient/LogHandler.swift +++ b/Sources/TelemetryClient/LogHandler.swift @@ -1,7 +1,7 @@ import Foundation -public struct LogHandler { - public enum LogLevel: Int, CustomStringConvertible { +public struct LogHandler: Sendable { + public enum LogLevel: Int, CustomStringConvertible, Sendable { case debug = 0 case info = 1 case error = 2 @@ -19,9 +19,9 @@ public struct LogHandler { } let logLevel: LogLevel - let handler: (LogLevel, String) -> Void + let handler: @Sendable (LogLevel, String) -> Void - public init(logLevel: LogHandler.LogLevel, handler: @escaping (LogHandler.LogLevel, String) -> Void) { + public init(logLevel: LogHandler.LogLevel, handler: @escaping @Sendable (LogHandler.LogLevel, String) -> Void) { self.logLevel = logLevel self.handler = handler } @@ -32,7 +32,7 @@ public struct LogHandler { } } - public static var stdout = { logLevel in + public static func stdout(_ logLevel: LogLevel) -> LogHandler { LogHandler(logLevel: logLevel) { level, message in print("[TelemetryDeck: \(level.description)] \(message)") } diff --git a/Sources/TelemetryClient/Presets/NavigationStatus.swift b/Sources/TelemetryClient/Presets/NavigationStatus.swift index b6fcde1..6ca100b 100644 --- a/Sources/TelemetryClient/Presets/NavigationStatus.swift +++ b/Sources/TelemetryClient/Presets/NavigationStatus.swift @@ -2,6 +2,7 @@ import Foundation /// This internal singleton keeps track of the last used navigation path so /// that the ``TelemetryDeck.navigationPathChanged(to:customUserID:)`` function has a `from` source to work off of. +@MainActor class NavigationStatus { static let shared = NavigationStatus() diff --git a/Sources/TelemetryClient/Presets/TelemetryDeck+Navigation.swift b/Sources/TelemetryClient/Presets/TelemetryDeck+Navigation.swift index 53bd499..ca97ae1 100644 --- a/Sources/TelemetryClient/Presets/TelemetryDeck+Navigation.swift +++ b/Sources/TelemetryClient/Presets/TelemetryDeck+Navigation.swift @@ -19,6 +19,7 @@ extension TelemetryDeck { /// - from: The navigation path at the beginning of the navigation event, identifying the view the user is leaving /// - to: The navigation path at the end of the navigation event, identifying the view the user is arriving at /// - customUserID: An optional string specifying a custom user identifier. If provided, it will override the default user identifier from the configuration. Default is `nil`. + @MainActor public static func navigationPathChanged(from source: String, to destination: String, customUserID: String? = nil) { NavigationStatus.shared.previousNavigationPath = destination @@ -57,17 +58,20 @@ extension TelemetryDeck { /// - Parameters: /// - to: The navigation path representing the view the user is arriving at. /// - customUserID: An optional string specifying a custom user identifier. If provided, it will override the default user identifier from the configuration. Default is `nil`. + @MainActor public static func navigationPathChanged(to destination: String, customUserID: String? = nil) { let source = NavigationStatus.shared.previousNavigationPath ?? "" Self.navigationPathChanged(from: source, to: destination, customUserID: customUserID) } + @MainActor @available(*, unavailable, renamed: "navigationPathChanged(from:to:customUserID:)") public static func navigate(from source: String, to destination: String, customUserID: String? = nil) { self.navigationPathChanged(from: source, to: destination, customUserID: customUserID) } + @MainActor @available(*, unavailable, renamed: "navigationPathChanged(to:customUserID:)") public static func navigate(to destination: String, customUserID: String? = nil) { self.navigationPathChanged(to: destination, customUserID: customUserID) diff --git a/Sources/TelemetryClient/Signal.swift b/Sources/TelemetryClient/Signal.swift index ead9f27..578e89e 100644 --- a/Sources/TelemetryClient/Signal.swift +++ b/Sources/TelemetryClient/Signal.swift @@ -41,6 +41,7 @@ internal struct SignalPostBody: Codable, Equatable { /// The default payload that is included in payloads processed by TelemetryDeck. public struct DefaultSignalPayload: Encodable { + @MainActor public static var parameters: [String: String] { var parameters: [String: String] = [ // deprecated names @@ -338,6 +339,7 @@ extension DefaultSignalPayload { } /// The current devices screen resolution width in points. + @MainActor static var screenResolutionWidth: String { #if os(iOS) || os(tvOS) return "\(UIScreen.main.bounds.width)" @@ -354,6 +356,7 @@ extension DefaultSignalPayload { } /// The current devices screen resolution height in points. + @MainActor static var screenResolutionHeight: String { #if os(iOS) || os(tvOS) return "\(UIScreen.main.bounds.height)" @@ -370,6 +373,7 @@ extension DefaultSignalPayload { } /// The current devices screen orientation. Returns `Fixed` for devices that don't support an orientation change. + @MainActor static var orientation: String { #if os(iOS) switch UIDevice.current.orientation { diff --git a/Sources/TelemetryClient/SignalCache.swift b/Sources/TelemetryClient/SignalCache.swift index 74e891c..cc46d4c 100644 --- a/Sources/TelemetryClient/SignalCache.swift +++ b/Sources/TelemetryClient/SignalCache.swift @@ -7,13 +7,13 @@ import Foundation /// correctly. /// /// Currently the cache is only in-memory. This will probably change in the near future. -internal class SignalCache where T: Codable { +internal class SignalCache: @unchecked Sendable where T: Codable { internal var logHandler: LogHandler? private var cachedSignals: [T] = [] private let maximumNumberOfSignalsToPopAtOnce = 100 - let queue = DispatchQueue(label: "telemetrydeck-signal-cache", attributes: .concurrent) + let queue = DispatchQueue(label: "com.telemetrydeck.SignalCache", attributes: .concurrent) /// How many Signals are cached func count() -> Int { diff --git a/Sources/TelemetryClient/SignalEnricher.swift b/Sources/TelemetryClient/SignalEnricher.swift index aedeb12..09bc411 100644 --- a/Sources/TelemetryClient/SignalEnricher.swift +++ b/Sources/TelemetryClient/SignalEnricher.swift @@ -1,6 +1,6 @@ import Foundation -public protocol SignalEnricher { +public protocol SignalEnricher: Sendable { func enrich( signalType: String, for clientUser: String?, diff --git a/Sources/TelemetryClient/SignalManager.swift b/Sources/TelemetryClient/SignalManager.swift index a30aac9..c61af7d 100644 --- a/Sources/TelemetryClient/SignalManager.swift +++ b/Sources/TelemetryClient/SignalManager.swift @@ -15,11 +15,12 @@ internal protocol SignalManageable { func attemptToSendNextBatchOfCachedSignals() } -internal class SignalManager: SignalManageable { +internal final class SignalManager: SignalManageable, @unchecked Sendable { internal static let minimumSecondsToPassBetweenRequests: Double = 10 private var signalCache: SignalCache let configuration: TelemetryManagerConfiguration + private var sendTimer: Timer? init(configuration: TelemetryManagerConfiguration) { @@ -74,35 +75,41 @@ internal class SignalManager: SignalManageable { customUserID: String?, configuration: TelemetryManagerConfiguration ) { - DispatchQueue.global(qos: .utility).async { - let enrichedMetadata: [String: String] = configuration.metadataEnrichers - .map { $0.enrich(signalType: signalName, for: customUserID, floatValue: floatValue) } - .reduce([String: String](), { $0.applying($1) }) - - let payload = DefaultSignalPayload.parameters - .applying(enrichedMetadata) - .applying(parameters) - - let signalPostBody = SignalPostBody( - receivedAt: Date(), - appID: UUID(uuidString: configuration.telemetryAppID)!, - clientUser: CryptoHashing.sha256(string: customUserID ?? self.defaultUserIdentifier, salt: configuration.salt), - sessionID: configuration.sessionID.uuidString, - type: "\(signalName)", - floatValue: floatValue, - payload: payload.toMultiValueDimension(), - isTestMode: configuration.testMode ? "true" : "false" - ) - - configuration.logHandler?.log(.debug, message: "Process signal: \(signalPostBody)") - - self.signalCache.push(signalPostBody) + DispatchQueue.main.async { + let defaultUserIdentifier = self.defaultUserIdentifier + let defaultParameters = DefaultSignalPayload.parameters + + DispatchQueue.global(qos: .utility).async { + let enrichedMetadata: [String: String] = configuration.metadataEnrichers + .map { $0.enrich(signalType: signalName, for: customUserID, floatValue: floatValue) } + .reduce([String: String](), { $0.applying($1) }) + + let payload = defaultParameters + .applying(enrichedMetadata) + .applying(parameters) + + let signalPostBody = SignalPostBody( + receivedAt: Date(), + appID: UUID(uuidString: configuration.telemetryAppID)!, + clientUser: CryptoHashing.sha256(string: customUserID ?? defaultUserIdentifier, salt: configuration.salt), + sessionID: configuration.sessionID.uuidString, + type: "\(signalName)", + floatValue: floatValue, + payload: payload.toMultiValueDimension(), + isTestMode: configuration.testMode ? "true" : "false" + ) + + configuration.logHandler?.log(.debug, message: "Process signal: \(signalPostBody)") + + self.signalCache.push(signalPostBody) + } } } /// Sends one batch of signals from the cache if not empty. /// If signals fail to send, we put them back into the cache to try again later. @objc + @Sendable internal func attemptToSendNextBatchOfCachedSignals() { configuration.logHandler?.log(.debug, message: "Current signal cache count: \(signalCache.count())") @@ -175,7 +182,7 @@ private extension SignalManager { // MARK: - Comms private extension SignalManager { - private func send(_ signalPostBodies: [SignalPostBody], completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) { + private func send(_ signalPostBodies: [SignalPostBody], completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) { DispatchQueue.global(qos: .utility).async { let path = "/api/v1/apps/\(self.configuration.telemetryAppID)/signals/multiple/" let url = self.configuration.apiBaseURL.appendingPathComponent(path) @@ -209,6 +216,7 @@ private extension SignalManager { #endif /// The default user identifier. If the platform supports it, the ``identifierForVendor``. Otherwise, a self-generated `UUID` which is persisted in custom `UserDefaults` if available. + @MainActor var defaultUserIdentifier: String { guard configuration.defaultUser == nil else { return configuration.defaultUser! } diff --git a/Sources/TelemetryClient/TelemetryClient.swift b/Sources/TelemetryClient/TelemetryClient.swift index 77e3599..91e5466 100644 --- a/Sources/TelemetryClient/TelemetryClient.swift +++ b/Sources/TelemetryClient/TelemetryClient.swift @@ -17,7 +17,7 @@ let telemetryClientVersion = "2.3.0" /// Use an instance of this class to specify settings for TelemetryManager. If these settings change during the course of /// your runtime, it might be a good idea to hold on to the instance and update it as needed. TelemetryManager's behaviour /// will update as well. -public final class TelemetryManagerConfiguration { +public struct TelemetryManagerConfiguration: Sendable { /// Your app's ID for Telemetry. Set this during initialization. public let telemetryAppID: String @@ -133,8 +133,6 @@ public final class TelemetryManagerConfiguration { /// Defaults to an empty array. public var metadataEnrichers: [SignalEnricher] = [] - private var lastDateAppEnteredBackground: Date = .distantPast - public init(appID: String, salt: String? = nil, baseURL: URL? = nil) { telemetryAppID = appID @@ -149,40 +147,6 @@ public final class TelemetryManagerConfiguration { } else { self.salt = "" } - - // initially start a new session upon app start (delayed so that `didSet` triggers) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.sessionID = UUID() - } - - // subscribe to notification to start a new session on app entering foreground (if needed) - #if os(iOS) || os(tvOS) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - NotificationCenter.default.addObserver(self, selector: #selector(self.willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) - } - #elseif os(watchOS) - if #available(watchOS 7.0, *) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - NotificationCenter.default.addObserver(self, selector: #selector(self.willEnterForeground), name: WKExtension.applicationWillEnterForegroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(self.didEnterBackground), name: WKExtension.applicationDidEnterBackgroundNotification, object: nil) - } - } else { - // Pre watchOS 7.0, this library will not use multiple sessions after backgrounding since there are no notifications we can observe. - } - #endif - } - - @objc func willEnterForeground() { - // check if at least 5 minutes have passed since last app entered background - if self.lastDateAppEnteredBackground.addingTimeInterval(5 * 60) < Date() { - // generate a new session identifier - sessionID = UUID() - } - } - - @objc func didEnterBackground() { - lastDateAppEnteredBackground = Date() } @available(*, deprecated, renamed: "sendSignalsInDebugConfiguration") @@ -195,7 +159,7 @@ public final class TelemetryManagerConfiguration { /// Accepts signals that signify events in your app's life cycle, collects and caches them, and pushes them to the Telemetry API. /// /// Use an instance of `TelemetryManagerConfiguration` to configure this at initialization and during its lifetime. -public class TelemetryManager { +public final class TelemetryManager: @unchecked Sendable { /// Returns `true` when the TelemetryManager already has been initialized correctly, `false` otherwise. public static var isInitialized: Bool { initializedTelemetryManager != nil @@ -341,22 +305,99 @@ public class TelemetryManager { } private init(configuration: TelemetryManagerConfiguration) { - self.configuration = configuration + self._configuration = configuration signalManager = SignalManager(configuration: configuration) + + self.startSessionAndObserveAppForegrounding() } private init(configuration: TelemetryManagerConfiguration, signalManager: SignalManageable) { - self.configuration = configuration + self._configuration = configuration self.signalManager = signalManager + + self.startSessionAndObserveAppForegrounding() } + nonisolated(unsafe) private static var initializedTelemetryManager: TelemetryManager? - private let configuration: TelemetryManagerConfiguration - private let signalManager: SignalManageable - private var lastTimeImmediateSyncRequested: Date = .distantPast + private let queue = DispatchQueue(label: "com.telemetrydeck.TelemetryManager", attributes: .concurrent) + + private var _configuration: TelemetryManagerConfiguration + private var configuration: TelemetryManagerConfiguration { + get { queue.sync(flags: .barrier) { return _configuration } } + set { queue.sync(flags: .barrier) { _configuration = newValue } } + } + + private var _lastTimeImmediateSyncRequested: Date = .distantPast + private var lastTimeImmediateSyncRequested: Date { + get { queue.sync(flags: .barrier) { return _lastTimeImmediateSyncRequested } } + set { queue.sync(flags: .barrier) { _lastTimeImmediateSyncRequested = newValue } } + } + + private var _lastDateAppEnteredBackground: Date = .distantPast + private var lastDateAppEnteredBackground: Date { + get { queue.sync(flags: .barrier) { return _lastDateAppEnteredBackground } } + set { queue.sync(flags: .barrier) { _lastDateAppEnteredBackground = newValue } } + } + + private func startSessionAndObserveAppForegrounding() { + // initially start a new session upon app start (delayed so that `didSet` triggers) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + TelemetryDeck.generateNewSession() + } + + // subscribe to notification to start a new session on app entering foreground (if needed) + #if os(iOS) || os(tvOS) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.willEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.didEnterBackground), + name: UIApplication.didEnterBackgroundNotification, + object: nil + ) + } + #elseif os(watchOS) + if #available(watchOS 7.0, *) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + NotificationCenter.default.addObserver( + self, + selector: #selector(self.willEnterForeground), + name: WKExtension.applicationWillEnterForegroundNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(self.didEnterBackground), + name: WKExtension.applicationDidEnterBackgroundNotification, + object: nil + ) + } + } else { + // Pre watchOS 7.0, this library will not use multiple sessions after backgrounding since there are no notifications we can observe. + } + #endif + } + + @objc func willEnterForeground() { + // check if at least 5 minutes have passed since last app entered background + if self.lastDateAppEnteredBackground.addingTimeInterval(5 * 60) < Date() { + // generate a new session identifier + TelemetryDeck.generateNewSession() + } + } + + @objc func didEnterBackground() { + lastDateAppEnteredBackground = Date() + } } @objc(TelemetryManagerConfiguration) diff --git a/Tests/TelemetryClientTests/LogHandlerTests.swift b/Tests/TelemetryClientTests/LogHandlerTests.swift index 2a7f0bd..20412c2 100644 --- a/Tests/TelemetryClientTests/LogHandlerTests.swift +++ b/Tests/TelemetryClientTests/LogHandlerTests.swift @@ -2,15 +2,16 @@ import XCTest final class LogHandlerTests: XCTestCase { + var counter: Int = 0 + var lastLevel: LogHandler.LogLevel? + func testLogHandler_stdoutLogLevelDefined() { XCTAssertEqual(LogHandler.stdout(.error).logLevel, .error) } func testLogHandler_logLevelRespected() { - var counter = 0 - let handler = LogHandler(logLevel: .info) { _, _ in - counter += 1 + self.counter += 1 } XCTAssertEqual(counter, 0) @@ -23,10 +24,8 @@ final class LogHandlerTests: XCTestCase { } func testLogHandler_defaultLogLevel() { - var lastLevel: LogHandler.LogLevel? - let handler = LogHandler(logLevel: .debug) { level, _ in - lastLevel = level + self.lastLevel = level } handler.log(message: "") diff --git a/Tests/TelemetryClientTests/TelemetryClientTests.swift b/Tests/TelemetryClientTests/TelemetryClientTests.swift index 83e1f62..360dfc5 100644 --- a/Tests/TelemetryClientTests/TelemetryClientTests.swift +++ b/Tests/TelemetryClientTests/TelemetryClientTests.swift @@ -61,7 +61,7 @@ final class TelemetryClientTests: XCTestCase { } } - let configuration = TelemetryManagerConfiguration(appID: UUID().uuidString) + var configuration = TelemetryManagerConfiguration(appID: UUID().uuidString) configuration.metadataEnrichers.append(BasicEnricher()) let signalManager = FakeSignalManager() @@ -81,7 +81,7 @@ final class TelemetryClientTests: XCTestCase { } } - let configuration = TelemetryManagerConfiguration(appID: UUID().uuidString) + var configuration = TelemetryManagerConfiguration(appID: UUID().uuidString) configuration.metadataEnrichers.append(BasicEnricher()) let signalManager = FakeSignalManager() @@ -111,7 +111,7 @@ final class TelemetryClientTests: XCTestCase { func testSendsSignals_withAnalyticsExplicitlyEnabled() { let YOUR_APP_ID = "44e0f59a-60a2-4d4a-bf27-1f96ccb4aaa3" - let configuration = TelemetryManagerConfiguration(appID: YOUR_APP_ID) + var configuration = TelemetryManagerConfiguration(appID: YOUR_APP_ID) configuration.analyticsDisabled = false let signalManager = FakeSignalManager() @@ -125,7 +125,7 @@ final class TelemetryClientTests: XCTestCase { func testDoesNotSendSignals_withAnalyticsExplicitlyDisabled() { let YOUR_APP_ID = "44e0f59a-60a2-4d4a-bf27-1f96ccb4aaa3" - let configuration = TelemetryManagerConfiguration(appID: YOUR_APP_ID) + var configuration = TelemetryManagerConfiguration(appID: YOUR_APP_ID) configuration.analyticsDisabled = true let signalManager = FakeSignalManager() @@ -141,7 +141,7 @@ final class TelemetryClientTests: XCTestCase { let YOUR_APP_ID = "44e0f59a-60a2-4d4a-bf27-1f96ccb4aaa3" - let configuration = TelemetryManagerConfiguration(appID: YOUR_APP_ID) + var configuration = TelemetryManagerConfiguration(appID: YOUR_APP_ID) configuration.analyticsDisabled = false let signalManager = FakeSignalManager() @@ -172,6 +172,7 @@ private class FakeSignalManager: SignalManageable { var processedSignalTypes = [String]() var processedSignals = [SignalPostBody]() + @MainActor func processSignal(_ signalType: String, parameters: [String : String], floatValue: Double?, customUserID: String?, configuration: TelemetryManagerConfiguration) { processedSignalTypes.append(signalType) let enrichedMetadata: [String: String] = configuration.metadataEnrichers