diff --git a/MVMCore/MVMCore.xcodeproj/project.pbxproj b/MVMCore/MVMCore.xcodeproj/project.pbxproj index 66f3849..67792ea 100644 --- a/MVMCore/MVMCore.xcodeproj/project.pbxproj +++ b/MVMCore/MVMCore.xcodeproj/project.pbxproj @@ -39,6 +39,8 @@ 0AEBB84625FA75C000EA80EE /* ActionOpenSMSModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEBB84525FA75C000EA80EE /* ActionOpenSMSModel.swift */; }; 0AFF597A23FC6E60005C24E8 /* ActionShareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFF597923FC6E60005C24E8 /* ActionShareModel.swift */; }; 1DAD0FFE26AAB40000216E83 /* ActionRunJavaScriptModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DAD0FFD26AAB3FF00216E83 /* ActionRunJavaScriptModel.swift */; }; + 2723337B28BD534D004EAEE0 /* MVMCoreEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2723337A28BD534D004EAEE0 /* MVMCoreEvent.swift */; }; + 2723337D28BD53C2004EAEE0 /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2723337C28BD53C2004EAEE0 /* Date+Extension.swift */; }; 30349BF11FCCA78A00546A1E /* MVMCoreSessionTimeHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 30349BEF1FCCA78A00546A1E /* MVMCoreSessionTimeHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 30349BF21FCCA78A00546A1E /* MVMCoreSessionTimeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 30349BF01FCCA78A00546A1E /* MVMCoreSessionTimeHandler.m */; }; 881D26931FCC9D180079C521 /* MVMCoreErrorObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 881D268F1FCC9D180079C521 /* MVMCoreErrorObject.m */; }; @@ -196,6 +198,8 @@ 0AEBB84525FA75C000EA80EE /* ActionOpenSMSModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionOpenSMSModel.swift; sourceTree = ""; }; 0AFF597923FC6E60005C24E8 /* ActionShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionShareModel.swift; sourceTree = ""; }; 1DAD0FFD26AAB3FF00216E83 /* ActionRunJavaScriptModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionRunJavaScriptModel.swift; sourceTree = ""; }; + 2723337A28BD534D004EAEE0 /* MVMCoreEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreEvent.swift; sourceTree = ""; }; + 2723337C28BD53C2004EAEE0 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = ""; }; 30349BEF1FCCA78A00546A1E /* MVMCoreSessionTimeHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreSessionTimeHandler.h; sourceTree = ""; }; 30349BF01FCCA78A00546A1E /* MVMCoreSessionTimeHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreSessionTimeHandler.m; sourceTree = ""; }; 881D268F1FCC9D180079C521 /* MVMCoreErrorObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreErrorObject.m; sourceTree = ""; }; @@ -435,6 +439,7 @@ 8876D5E51FB50AB000EB2E3D /* UIFont+MFSpacing.m */, 8876D5E61FB50AB000EB2E3D /* UILabel+MFCustom.h */, 8876D5E71FB50AB000EB2E3D /* UILabel+MFCustom.m */, + 2723337C28BD53C2004EAEE0 /* Date+Extension.swift */, ); path = Categories; sourceTree = ""; @@ -687,6 +692,7 @@ AFEEE81C1FCDF3CA00B5EDD0 /* MVMCoreLoggingHandler.h */, AFEEE81D1FCDF3CA00B5EDD0 /* MVMCoreLoggingHandler.m */, D288D5F426C6EFE000A5C365 /* MVMCoreLoggingHandler+Extension.swift */, + 2723337A28BD534D004EAEE0 /* MVMCoreEvent.swift */, ); path = OtherHandlers; sourceTree = ""; @@ -912,6 +918,7 @@ AFBB96901FBA3A9A0008D868 /* MVMCoreNavigationObject.m in Sources */, 1DAD0FFE26AAB40000216E83 /* ActionRunJavaScriptModel.swift in Sources */, 946EE1AB237B5C940036751F /* Decoder.swift in Sources */, + 2723337D28BD53C2004EAEE0 /* Date+Extension.swift in Sources */, AF70699A287DD02400077CF6 /* ActionContactHandler.swift in Sources */, AF69D4F3286E9DCE00BC6862 /* ActionActionsHandler.swift in Sources */, 946EE1BC237B691A0036751F /* ActionOpenPageModel.swift in Sources */, @@ -950,6 +957,7 @@ 01F2A05223A8325100D954D8 /* ModelMapping.swift in Sources */, 8876D5F51FB50AB000EB2E3D /* UILabel+MFCustom.m in Sources */, AFBB96B31FBA3B590008D868 /* MVMCoreGetterUtility.m in Sources */, + 2723337B28BD534D004EAEE0 /* MVMCoreEvent.swift in Sources */, AF43A7071FC4D7A2008E9347 /* MVMCoreObject.m in Sources */, 94C014D924212360005811A9 /* ActionSettingModel.swift in Sources */, D2DEDCB723C63F3B00C44CC4 /* Clamping.swift in Sources */, diff --git a/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift b/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift index 7bdc502..3c0887e 100644 --- a/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift +++ b/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift @@ -44,7 +44,11 @@ open class ActionOpenPageHandler: MVMCoreJSONActionHandlerProtocol { open func performRequestAddingClientParameters(with requestParameters: MVMCoreRequestParameters, model: ActionOpenPageModel, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws -> MVMCoreLoadRequestOperation? { // Adds any client parameters to the request parameters. if let parametersToFetch = model.clientParameters, - let fetchedParameters = try await ClientParameterHandler().getClientParameters(with: parametersToFetch, requestParameters: requestParameters.parameters as? [String : Any] ?? [:], showLoadingOverlay: !requestParameters.backgroundRequest) { + let fetchedParameters = try await ClientParameterHandler().getClientParameters( + with: parametersToFetch, + requestParameters: requestParameters.parameters as? [String : Any] ?? [:], + actionId: MVMCoreActionHandler.getUUID(additionalData: additionalData) ?? "unknown", + showLoadingOverlay: !requestParameters.backgroundRequest) { requestParameters.add(fetchedParameters) } try Task.checkCancellation() @@ -68,7 +72,7 @@ open class ActionOpenPageHandler: MVMCoreJSONActionHandlerProtocol { public extension ClientParameterHandler { /// Iterates through the clientParameters list. Gets values from the individual handlers and attaches the parameters to extraParameters. - func getClientParameters(with model: ClientParameterModel, requestParameters: [String: Any], showLoadingOverlay: Bool) async throws -> [String: Any]? { + func getClientParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String, showLoadingOverlay: Bool) async throws -> [String: Any]? { if showLoadingOverlay { MVMCoreLoadingOverlayHandler.sharedLoadingOverlay()?.startLoading() } @@ -79,7 +83,7 @@ public extension ClientParameterHandler { } return try await withCheckedThrowingContinuation({ continuation in do { - try getParameters(with: model, requestParameters: requestParameters) { parameters in + try getParameters(with: model, requestParameters: requestParameters, actionId: actionId) { parameters in continuation.resume(returning: parameters) } } catch { diff --git a/MVMCore/MVMCore/ActionHandling/MVMCoreActionHandler.swift b/MVMCore/MVMCore/ActionHandling/MVMCoreActionHandler.swift index 398a590..c37d7a4 100644 --- a/MVMCore/MVMCore/ActionHandling/MVMCoreActionHandler.swift +++ b/MVMCore/MVMCore/ActionHandling/MVMCoreActionHandler.swift @@ -84,7 +84,7 @@ public protocol MVMCoreJSONActionHandlerProtocol: MVMCoreActionHandlerProtocol { /// Handle an action with the given model. open func handleAction(with model: ActionModelProtocol, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) async throws { try Task.checkCancellation() - var additionalData = MVMCoreActionHandler.setUUID(additionalData: additionalData) + var (additionalData, uuid) = MVMCoreActionHandler.setUUID(additionalData: additionalData) MVMCoreActionHandler.log(string: "Begin Action: \(model.actionType)", additionalData: additionalData) defer { MVMCoreActionHandler.log(string: "End Action: \(model.actionType)", additionalData: additionalData) @@ -97,17 +97,20 @@ public protocol MVMCoreJSONActionHandlerProtocol: MVMCoreActionHandlerProtocol { do { let handlerType = try ModelRegistry.getHandler(model) as! MVMCoreActionHandlerProtocol.Type let handler = handlerType.init() + MVMCoreLoggingHandler.shared()?.logCoreEvent(.actionInvoked(name: model.actionType, pageType: pageType(from: delegateObject), uuid: uuid)) if let handler = handler as? MVMCoreJSONActionHandlerProtocol { // Needed until we can remove legacy delegate functions. try await handler.performAction(with: json, model: model, delegateObject: delegateObject, additionalData: additionalData) } else { try await handler.execute(with: model, delegateObject: delegateObject, additionalData: additionalData) } + MVMCoreLoggingHandler.shared()?.logCoreEvent(.actionComplete(name: model.actionType, pageType: pageType(from: delegateObject), uuid: uuid)) } catch ModelRegistry.Error.handlerNotMapped { try Task.checkCancellation() // Allows custom handling if there no handler for the action. guard try await handleUnregisteredAction(with: model, json: json, additionalData: additionalData, delegateObject: delegateObject) else { MVMCoreActionHandler.log(string: "Failed Action Unknown", additionalData: additionalData) + MVMCoreLoggingHandler.shared()?.logCoreEvent(.actionNotFound(name: model.actionType , pageType: pageType(from: delegateObject))) throw ActionError.unknownAction(type: model.actionType) } } catch { @@ -132,12 +135,13 @@ public protocol MVMCoreJSONActionHandlerProtocol: MVMCoreActionHandlerProtocol { // MARK: - Legacy Holdovers - static public func setUUID(additionalData: [AnyHashable: Any]?, force: Bool = false) -> [AnyHashable: Any] { - if !force && getUUID(additionalData: additionalData) != nil { return additionalData! } - return additionalData.dictionaryAdding(key: "Action-UUID", value: UUID().uuidString) + static public func setUUID(additionalData: [AnyHashable: Any]?, force: Bool = false) -> ([AnyHashable: Any], String) { + if !force, let uuid = getUUID(additionalData: additionalData) { return (additionalData!, uuid) } + let newUUID = UUID().uuidString + return (additionalData.dictionaryAdding(key: "Action-UUID", value: newUUID), newUUID) } - static public func getUUID(additionalData: [AnyHashable: Any]?) -> String? { + @objc static public func getUUID(additionalData: [AnyHashable: Any]?) -> String? { return additionalData?.optionalStringForKey("Action-UUID") } @@ -145,6 +149,17 @@ public protocol MVMCoreJSONActionHandlerProtocol: MVMCoreActionHandlerProtocol { MVMCoreLoggingHandler.logDebugMessage(withDelegate: "ActionHandler: UUID: \(String(describing: getUUID(additionalData: additionalData))), \(string)") } + fileprivate func logActionError(_ error: Error, _ actionType: String?, _ additionalData: [AnyHashable: Any]?, _ delegateObject: DelegateObject?) { + MVMCoreActionHandler.log(string: "Failed Action: Error \(error)", additionalData: additionalData) + if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: MVMCoreActionHandler.getErrorLocation(with: delegateObject?.actionDelegate, actionType: actionType ?? "noAction")) { + defaultHandleActionError(errorObject, additionalData: additionalData) + } + } + + fileprivate func pageType(from delegateObject: DelegateObject?) -> String { + return (delegateObject?.loadDelegate as? MVMCoreViewControllerProtocol)?.pageType ?? "unknown" + } + /// Legacy handle action with json. @objc(handleActionWithDictionary:additionalData:delegateObject:) open func handleAction(with json: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) { @@ -159,16 +174,17 @@ public protocol MVMCoreJSONActionHandlerProtocol: MVMCoreActionHandlerProtocol { } catch { let actionType = json?.optionalStringForKey(KeyActionType) switch error { + case ModelRegistry.Error.decoderError, is DecodingError: + MVMCoreLoggingHandler.shared()?.logCoreEvent(.actionFailedToDecode(pageType: pageType(from: delegateObject), error: error)) + logActionError(error, actionType, additionalData, delegateObject) case ModelRegistry.Error.decoderErrorModelNotMapped: // If the model is not mapped, give the legacy classes a chance to handle it. if try await handleUnregisteredAction(with: nil, json: json!, additionalData: additionalData, delegateObject: delegateObject) == false { - fallthrough + MVMCoreLoggingHandler.shared()?.logCoreEvent(.actionNotFound(name: actionType ?? "noAction", pageType: pageType(from: delegateObject))) + logActionError(error, actionType, additionalData, delegateObject) } default: - MVMCoreActionHandler.log(string: "Failed Action: Error \(error)", additionalData: additionalData) - if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: MVMCoreActionHandler.getErrorLocation(with: delegateObject?.actionDelegate, actionType: actionType ?? "noAction")) { - defaultHandleActionError(errorObject, additionalData: additionalData) - } + logActionError(error, actionType, additionalData, delegateObject) } } } @@ -177,8 +193,8 @@ public protocol MVMCoreJSONActionHandlerProtocol: MVMCoreActionHandlerProtocol { /// Bridges the legacy json using functions and the new model using functions. open func handleAction(with model: ActionModelProtocol, json: [AnyHashable: Any], additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?) async throws { try Task.checkCancellation() - var additionalData = additionalData.dictionaryAdding(key: jsonKey, value: json) - additionalData = MVMCoreActionHandler.setUUID(additionalData: additionalData, force: true) + let additionalDataWithJSON = additionalData.dictionaryAdding(key: jsonKey, value: json) + let (additionalData, _) = MVMCoreActionHandler.setUUID(additionalData: additionalDataWithJSON, force: true) MVMCoreActionHandler.log(string: "JSON \(json)", additionalData: additionalData) if let closure = (delegateObject?.actionDelegate as? ActionDelegateProtocol)?.performAction { // Allow newer delegates to handle calls from legacy functions diff --git a/MVMCore/MVMCore/Categories/Date+Extension.swift b/MVMCore/MVMCore/Categories/Date+Extension.swift new file mode 100644 index 0000000..3f7df53 --- /dev/null +++ b/MVMCore/MVMCore/Categories/Date+Extension.swift @@ -0,0 +1,16 @@ +// +// Date+Extension.swift +// MVMCore +// +// Created by Kyle on 8/29/22. +// Copyright © 2022 myverizon. All rights reserved. +// + +import Foundation + +public extension Date { + + static func unixMillisecondsNow() -> Int64 { + return Int64(Self().timeIntervalSince1970 * 1000) + } +} diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 8ead4e1..a53c062 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -7,10 +7,8 @@ // @objcMembers open class ClientParameterHandler: NSObject { - - var parameterHandlerList: [ClientParameterProtocol] = [] - let parametersWorkQueue = DispatchQueue(label: "com.mva.clientparameter") - let group = DispatchGroup() + + static let DefaultTimeout = 30.0 open func createParametersHandler(_ clientParameterModel: ClientParameterModelProtocol) -> ClientParameterProtocol? { do { @@ -43,38 +41,41 @@ /// ] ///} /// completionHandler can return flat dictinary or a map. It depends on the paramters handler - open func getParameters(with clientParameters: [String: Any], requestParameters: [String: Any], completionHandler:@escaping ([String: Any]?) -> ()) throws { + open func getParameters(with clientParameters: [String: Any], requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: Any]?) -> ()) throws { guard let clientParameterModel = try getClientParameterModel(clientParameters) else { completionHandler(nil) return } - try getParameters(with: clientParameterModel, requestParameters: requestParameters, completionHandler: completionHandler) + try getParameters(with: clientParameterModel, requestParameters: requestParameters, actionId: actionId, completionHandler: completionHandler) } - open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], completionHandler:@escaping ([String: Any]?) -> ()) throws { - - let timeout = model.timeout ?? 30.0 - + open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: Any]?) -> ()) throws { + + let parametersWorkQueue = DispatchQueue(label: "com.mva.clientparameter") + let group = DispatchGroup() + let timeout = model.timeout ?? Self.DefaultTimeout + + let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } + let requestUUID = [0.. [String: AnyHashable] in + guard let parameter = element else { + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( + name: model.list[index].type, + uuid: requestUUID[index], + actionId: actionId)) + return parameterHandlerList[index].valueOnTimeout() + } + return parameter + }.reduce(into: [String: AnyHashable]()) { partialResult, next in + partialResult.merge(next) { first, last in first } } - return parametersList } // Setup completion handlers. Barriered to ensure one happens after the other. @@ -92,28 +93,30 @@ } // Setup timeout. - self.parametersWorkQueue.asyncAfter(deadline: .now() + .seconds(Int(timeout)), execute: timeoutWorkItem) + parametersWorkQueue.asyncAfter(deadline: .now() + .seconds(Int(timeout)), execute: timeoutWorkItem) // Setup the parameter execution. - for (index, parameterHandler) in self.parameterHandlerList.enumerated() { + for (index, parameterHandler) in parameterHandlerList.enumerated() { let parameterType = parameterHandler.clientParameterModel.type - self.group.enter() + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch(name: parameterType, uuid: requestUUID[index], actionId: actionId)) + group.enter() parameterHandler.fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) { (receivedParameter) in // Queue the results for merge. - self.parametersWorkQueue.async { + parametersWorkQueue.async { if (returnedList[index] != nil) { MVMCoreLoggingHandler.addError(toLog: MVMCoreErrorObject(title: nil, message: "Client parameter \(parameterType) has already executed. The completion handler should only be called once!", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: String(describing: ClientParameterHandler.self))!) } else { + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete(name: parameterType, uuid: requestUUID[index], actionId: actionId)) returnedList[index] = receivedParameter - self.group.leave() // Leaving is only done after setup (barriered). + group.leave() // Leaving is only done after setup (barriered). } } } } // Callback when all parameters have been merged. - self.group.notify(queue: self.parametersWorkQueue, work: completionWorkItem); + group.notify(queue: parametersWorkQueue, work: completionWorkItem); } } } diff --git a/MVMCore/MVMCore/OtherHandlers/MVMCoreEvent.swift b/MVMCore/MVMCore/OtherHandlers/MVMCoreEvent.swift new file mode 100644 index 0000000..0753ff7 --- /dev/null +++ b/MVMCore/MVMCore/OtherHandlers/MVMCoreEvent.swift @@ -0,0 +1,126 @@ +// +// MVMCoreEvent.swift +// MVMCore +// +// https://oneconfluence.verizon.com/display/MFD/NewRelic+Client+Event+Logging +// +// Created by Kyle on 8/29/22. +// Copyright © 2022 myverizon. All rights reserved. +// + +import Foundation + +/// A list of possible events from the app. +public enum MVMCoreEvent { + + // ---------------------------- + // MARK: Action Events + // ---------------------------- + + /// Failed to decode the action payload. + case actionFailedToDecode( + pageType: String, + error: Error? + ) + + /// Could not find the action specified.. + case actionNotFound( + name: String, + pageType: String + ) + + /// The webview bridge action handler was invoked and is in progress. + case actionInvoked( + name: String, + pageType: String, + uuid: String + ) + + /// The action failed.. + case actionFailed( + name: String, + pageType: String, + uuid: String + ) + + /// The action is completed. + case actionComplete( + name: String, + pageType: String, + uuid: String + ) + + // ---------------------------- + // MARK: ClientParameter Events + // ---------------------------- + + /// Could not find the client parameter specified. + case clientParameterNotFound( + name: String, + actionId: String + ) + + /// The client perameter handler was invoked and is in progress. + case clientParameterStartFetch( + name: String, + uuid: String, + actionId: String + ) + + /// The client perameter handler timed out and is returning a default value. + case clientParameterTimeout( + name: String, + uuid: String, + actionId: String + ) + + /// The client paramter fetch completed. + case clientParameterFetchComplete( + name: String, + uuid: String, + actionId: String + ) + + public enum EventType: String { + case action + case clientParameter + + public var notification: Notification.Name { + return Notification.Name(rawValue: rawValue) + } + } + + public var type: EventType { + switch self { + case .actionFailedToDecode: return .action + case .actionNotFound: return .action + case .actionInvoked: return .action + case .actionFailed: return .action + case .actionComplete: return .action + case .clientParameterNotFound: return .clientParameter + case .clientParameterStartFetch: return .clientParameter + case .clientParameterTimeout: return .clientParameter + case .clientParameterFetchComplete: return .clientParameter + } + } + + public var name: String { + let name = String(describing: self) + if let attribIndex = name.firstIndex(of: "(") { + return String(name[.. *)loadedViewController error:(nullable MVMCoreErrorObject *)error; + (void)logAlertForAlertController:(nullable MVMCoreAlertController *)alertController; +- (void)recordEvent:(nonnull NSString *)name attributes:(nullable NSDictionary *)attributes; #pragma mark MVMCoreLoggingDelegateProtocol - (void)handleDebugMessage:(nullable NSString *)message; diff --git a/MVMCore/MVMCore/OtherHandlers/MVMCoreLoggingHandler.m b/MVMCore/MVMCore/OtherHandlers/MVMCoreLoggingHandler.m index 766ee3d..9765925 100644 --- a/MVMCore/MVMCore/OtherHandlers/MVMCoreLoggingHandler.m +++ b/MVMCore/MVMCore/OtherHandlers/MVMCoreLoggingHandler.m @@ -51,6 +51,8 @@ } } +- (void)recordEvent:(nonnull NSString *)name attributes:(nullable NSDictionary *)attributes {} + #pragma mark - logging delegate - (void)handleDebugMessage:(nullable NSString *)message {