Merge branch 'feature/new_relic_action_tracking' into 'develop'

create core events for actions and clientparameters tracking

See merge request BPHV_MIPS/mvm_core!225
This commit is contained in:
Hedden, Kyle Matthew 2022-09-19 21:52:09 +00:00
commit 3ecd769f1c
8 changed files with 222 additions and 46 deletions

View File

@ -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 = "<group>"; };
0AFF597923FC6E60005C24E8 /* ActionShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionShareModel.swift; sourceTree = "<group>"; };
1DAD0FFD26AAB3FF00216E83 /* ActionRunJavaScriptModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionRunJavaScriptModel.swift; sourceTree = "<group>"; };
2723337A28BD534D004EAEE0 /* MVMCoreEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreEvent.swift; sourceTree = "<group>"; };
2723337C28BD53C2004EAEE0 /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = "<group>"; };
30349BEF1FCCA78A00546A1E /* MVMCoreSessionTimeHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreSessionTimeHandler.h; sourceTree = "<group>"; };
30349BF01FCCA78A00546A1E /* MVMCoreSessionTimeHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreSessionTimeHandler.m; sourceTree = "<group>"; };
881D268F1FCC9D180079C521 /* MVMCoreErrorObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreErrorObject.m; sourceTree = "<group>"; };
@ -435,6 +439,7 @@
8876D5E51FB50AB000EB2E3D /* UIFont+MFSpacing.m */,
8876D5E61FB50AB000EB2E3D /* UILabel+MFCustom.h */,
8876D5E71FB50AB000EB2E3D /* UILabel+MFCustom.m */,
2723337C28BD53C2004EAEE0 /* Date+Extension.swift */,
);
path = Categories;
sourceTree = "<group>";
@ -687,6 +692,7 @@
AFEEE81C1FCDF3CA00B5EDD0 /* MVMCoreLoggingHandler.h */,
AFEEE81D1FCDF3CA00B5EDD0 /* MVMCoreLoggingHandler.m */,
D288D5F426C6EFE000A5C365 /* MVMCoreLoggingHandler+Extension.swift */,
2723337A28BD534D004EAEE0 /* MVMCoreEvent.swift */,
);
path = OtherHandlers;
sourceTree = "<group>";
@ -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 */,

View File

@ -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 {

View File

@ -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

View File

@ -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)
}
}

View File

@ -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..<parameterHandlerList.count].map { _ in UUID().uuidString }
var returnedList = [[String: AnyHashable]?](repeating: nil, count: parameterHandlerList.count)
// Dispatch setup on queue to ensure setup is complete before completion callbacks.
// Don't use [weak self]. Object is deallocated in the dispatch queue.
parametersWorkQueue.async(group: group, qos: .userInitiated) {
// Create the handler list so that same object can be used when merging. Merging needs default value in case of timeout
for parameterModel in model.list {
if let parameterHandler = self.createParametersHandler(parameterModel) {
self.parameterHandlerList.append(parameterHandler)
}
}
var returnedList = [[String: AnyHashable]?](repeating: nil, count: self.parameterHandlerList.count)
var mergedParametersList: [String: AnyHashable] {
var parametersList: [String: AnyHashable] = [:]
for (index, item) in returnedList.enumerated() {
let parameter = item ?? self.parameterHandlerList[index].valueOnTimeout()
parametersList = parametersList.merging(parameter) { (_, new) in new }
return returnedList.enumerated().map { (index, element) -> [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);
}
}
}

View File

@ -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[..<attribIndex])
}
return name
}
public var notification: Notification.Name {
return Notification.Name(rawValue: type.rawValue)
}
}
extension MVMCoreLoggingHandler {
func logCoreEvent(_ event: MVMCoreEvent, at timestamp: Int64 = Date.unixMillisecondsNow()) {
recordEvent(event.type.rawValue, attributes: ["timestamp": timestamp, "event": event])
}
}

View File

@ -25,6 +25,7 @@
+ (void)logWithDelegateWithObject:(nullable id)object withName:(nullable NSString *)name withExtraInfo:(nullable NSDictionary *)extra;
+ (void)logWithDelegateLoadFinished:(nullable MVMCoreLoadObject *)loadObject loadedViewController:(nullable UIViewController <MVMCoreViewControllerProtocol> *)loadedViewController error:(nullable MVMCoreErrorObject *)error;
+ (void)logAlertForAlertController:(nullable MVMCoreAlertController *)alertController;
- (void)recordEvent:(nonnull NSString *)name attributes:(nullable NSDictionary<NSString *, id> *)attributes;
#pragma mark MVMCoreLoggingDelegateProtocol
- (void)handleDebugMessage:(nullable NSString *)message;

View File

@ -51,6 +51,8 @@
}
}
- (void)recordEvent:(nonnull NSString *)name attributes:(nullable NSDictionary<NSString *, id> *)attributes {}
#pragma mark - logging delegate
- (void)handleDebugMessage:(nullable NSString *)message {