Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions Sources/CheckoutCardManagement/CheckoutCardManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,17 @@ public final class CheckoutCardManager: CardManager {
walletCardsList: walletCardsList) { [weak self] in
switch $0 {
case .success:
let event = LogEvent.configurePushProvisioning(last4CardholderID: String(cardholderID.suffix(4)))
let event = LogEvent.configurePushProvisioning(cardholderId: cardholderID)
self?.logger?.log(event, startedAt: startTime)
completionHandler(.success)
case .failure(let networkError):
self?.logger?.log(.failure(source: "Configure Push Provisioning", error: networkError), startedAt: startTime)
self?.logger?.log(
.failure(source: "Configure Push Provisioning",
error: networkError,
networkError: networkError,
additionalInfo: ["cardholderId": cardholderID]),
startedAt: startTime
)
completionHandler(.failure(.from(networkError)))
}
}
Expand All @@ -133,11 +139,16 @@ public final class CheckoutCardManager: CardManager {
let cards = cards.compactMap {
Card(networkCard: $0, manager: self)
}
let cardsSuffixes: [String] = cards.compactMap { $0.partIdentifier }
self?.logger?.log(.cardList(idSuffixes: cardsSuffixes), startedAt: startTimestamp)
let cardIds: [String] = cards.compactMap { $0.id }
self?.logger?.log(.cardList(cardIds: cardIds), startedAt: startTimestamp)
completionHandler(.success(cards))
case .failure(let networkError):
self?.logger?.log(.failure(source: "Get Cards", error: networkError), startedAt: startTimestamp)
self?.logger?.log(
.failure(source: "Get Cards",
error: networkError,
additionalInfo: [:]),
startedAt: startTimestamp
)
completionHandler(.failure(.from(networkError)))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import CheckoutEventLoggerKit
import CheckoutCardNetwork

final class CheckoutLogger: NetworkLogger {

let sessionID: String
private let eventLogger: CheckoutEventLogging

Expand All @@ -26,11 +25,11 @@ final class CheckoutLogger: NetworkLogger {

/// Network Logger conformance, enabling to collect network level details if error is encountered
func log(error: Error, additionalInfo: [String: String]) {
let event = LogEvent.failure(source: additionalInfo["source"] ?? "", error: error)
let event = LogEvent.failure(source: additionalInfo["source"] ?? "", error: error, additionalInfo: additionalInfo)
eventLogger.log(event: LogFormatter.build(event: event,
extraProperties: additionalInfo))
extraProperties: additionalInfo))
}

/// Enable logger to dispatch events
func setupRemoteLogging(environment: CardManagerEnvironment,
serviceVersion: CardServiceVersion) {
Expand Down
25 changes: 14 additions & 11 deletions Sources/CheckoutCardManagement/LoggingSupport/LogEvent.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// LogEvent.swift
//
//
//
// Created by Alex Ioja-Yang on 12/09/2022.
//
Expand All @@ -16,32 +16,35 @@ enum LogEvent {
case initialized(design: CardManagementDesignSystem)

/// Describe a successful retrieval of the card list
case cardList(idSuffixes: [String])
case cardList(cardIds: [String])

/// Describe a successful call to retrieve a pin
case getPin(idLast4: String, cardState: CardState)
case getPin(cardId: String, cardState: CardState)

/// Describe a successful call to retrieve a card number
case getPan(cardId: String, cardState: CardState)

/// Describe a successful call to retrieve a card number
case getPan(idLast4: String, cardState: CardState)
case copyPan(cardId: String, cardState: CardState)

/// Describe a successful call to retrieve a security code
case getCVV(idLast4: String, cardState: CardState)
case getCVV(cardId: String, cardState: CardState)

/// Describe a successful call to retrieve a pan and a security code
case getPanCVV(idLast4: String, cardState: CardState)
case getPanCVV(cardId: String, cardState: CardState)

/// Describe a successfull event where a card state change was completed
case stateManagement(idLast4: String, originalState: CardState, requestedState: CardState, reason: String?)
case stateManagement(cardId: String, originalState: CardState, requestedState: CardState, reason: String?)

/// Describe a successfull Configuration of Push Provisioning
case configurePushProvisioning(last4CardholderID: String)
case configurePushProvisioning(cardholderId: String)

/// Describe a Get Card Digitization State event
case getCardDigitizationState(last4CardID: String, digitizationState: DigitizationState)
case getCardDigitizationState(cardId: String, digitizationState: DigitizationState)

/// Describe a Push Provisioning event
case pushProvisioning(last4CardID: String)
case pushProvisioning(cardId: String)

/// Describe an unexpected but non critical failure
case failure(source: String, error: Error)
case failure(source: String, error: Error, networkError: CardNetworkError? = nil, additionalInfo: [String: Any])
}
145 changes: 125 additions & 20 deletions Sources/CheckoutCardManagement/LoggingSupport/LogFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import CheckoutEventLoggerKit
import CheckoutCardNetwork

/// Formatter for internal Analytic Events
enum LogFormatter {
Expand Down Expand Up @@ -41,6 +42,7 @@ enum LogFormatter {
case .getCardDigitizationState: return "get_card_digitization_state"
case .pushProvisioning: return "push_provisioning"
case .failure: return "failure"
case .copyPan: return "copy_pan"
}
}

Expand All @@ -56,6 +58,7 @@ enum LogFormatter {
.stateManagement,
.configurePushProvisioning,
.getCardDigitizationState,
.copyPan,
.pushProvisioning:
return .info
case .failure:
Expand All @@ -72,45 +75,147 @@ enum LogFormatter {
"version": AnyCodable(Constants.productVersion),
"design": AnyCodable(try? design.mapToLogDictionary())
]
case .cardList(let suffixes):
dictionary = ["suffix_ids": AnyCodable(suffixes)]
case .getPin(let idLast4, let state),
.getPan(let idLast4, let state),
.getCVV(let idLast4, let state),
.getPanCVV(let idLast4, let state):
case .cardList(let cardIds):
dictionary = ["cardIds": AnyCodable(cardIds)]
case .getPin(let cardId, let state),
.getPan(let cardId, let state),
.getCVV(let cardId, let state),
.getPanCVV(let cardId, let state),
.copyPan(let cardId, let state):
dictionary = [
"suffix_id": AnyCodable(idLast4),
"cardId": AnyCodable(cardId),
"card_state": AnyCodable(state.rawValue)
]
case .stateManagement(let idLast4, let originalState, let requestedState, let reason):
case .stateManagement(let cardId, let originalState, let requestedState, let reason):
dictionary = [
"suffix_id": AnyCodable(idLast4),
"cardId": AnyCodable(cardId),
"from": AnyCodable(originalState.rawValue),
"to": AnyCodable(requestedState.rawValue)
]
if let reason {
dictionary["reason"] = AnyCodable(reason)
}
case .configurePushProvisioning(let last4CardholderID):
case .configurePushProvisioning(let cardholderId):
dictionary = [
"cardholder": AnyCodable(last4CardholderID)
"cardholder": AnyCodable(cardholderId)
]
case .getCardDigitizationState(let last4CardID, let digitizationState):
case .getCardDigitizationState(let cardId, let digitizationState):
dictionary = [
"card": AnyCodable(last4CardID),
"card": AnyCodable(cardId),
"digitization_state": AnyCodable(digitizationState.rawValue)
]
case .pushProvisioning(let last4CardID):
case .pushProvisioning(let cardId):
dictionary = [
"card": AnyCodable(last4CardID)
]
case .failure(let source, let error):
dictionary = [
"source": AnyCodable(source),
"error": AnyCodable(error.localizedDescription)
"cardId": AnyCodable(cardId)
]
case .failure(let source, let error, let networkError, let additionalInfo):
if let networkError = networkError {
let errorDetails = extractErrorDetails(from: networkError)
dictionary = [
"source": AnyCodable(source),
"error_type": AnyCodable(errorDetails.type),
"error_description": AnyCodable(errorDetails.description)
]

if let additionalErrorInfo = errorDetails.additionalInfo {
additionalErrorInfo.forEach { key, value in
dictionary["error_\(key)"] = AnyCodable(value)
}
}
} else {
dictionary = [
"source": AnyCodable(source),
"error": AnyCodable(error.localizedDescription)
]
}

additionalInfo.forEach { (key: String, value: Any) in
dictionary[key] = AnyCodable(value)
}
}
dictionary.addTimeSince(startDate: startDate)
return dictionary
}

/// Extract structured information from CardNetworkError
private static func extractErrorDetails(from error: Error) -> (type: String, description: String, additionalInfo: [String: Any]?) {
guard let cardNetworkError = error as? CardNetworkError else {
return (type: "unknown", description: error.localizedDescription, additionalInfo: nil)
}

switch cardNetworkError {
case .authenticationFailure:
return (type: "authentication_failure", description: "Authentication failed", additionalInfo: nil)

case .deviceNotSupported:
return (type: "device_not_supported", description: "Device does not support the operation", additionalInfo: nil)

case .insecureDevice:
return (type: "insecure_device", description: "Device flagged as unsafe", additionalInfo: nil)

case .invalidRequest(let hint):
return (type: "invalid_request", description: "Invalid request", additionalInfo: ["hint": hint])

case .invalidRequestInput:
return (type: "invalid_request_input", description: "Invalid request input format", additionalInfo: nil)

case .misconfigured(let hint):
return (type: "misconfigured", description: "Service connection misconfigured", additionalInfo: ["hint": hint])

case .serverIssue:
return (type: "server_issue", description: "Server unable to respond", additionalInfo: nil)

case .unauthenticated:
return (type: "unauthenticated", description: "Session expired or missing", additionalInfo: nil)

case .secureOperationsFailure:
return (type: "secure_operations_failure", description: "Unable to handle secure operations", additionalInfo: nil)

case .parsingFailure:
return (type: "parsing_failure", description: "Response format mismatch", additionalInfo: nil)

case .pushProvisioningFailure(let failure):
let failureType = extractPushProvisioningFailureType(failure)
return (type: "push_provisioning_failure", description: "Push provisioning failed", additionalInfo: ["failure_type": failureType])

case .fetchDigitizationStateFailure(let failure):
let failureType = extractDigitizationStateFailureType(failure)
return (type: "fetch_digitization_state_failure", description: "Failed to fetch digitization state", additionalInfo: ["failure_type": failureType])
case .unableToCopy(let failure):
let failureType = extractUnableToCopyFailureType(failure)
return (type: "copy_failure", description: "Failed to copy to clipboard", additionalInfo: ["failure_type": failureType])
}
}

/// Extract push provisioning failure type
private static func extractPushProvisioningFailureType(_ failure: CardNetworkError.PushProvisioningFailure) -> String {
switch failure {
case .cancelled:
return "cancelled"
case .configurationFailure:
return "configuration_failure"
case .operationFailure(let hint):
return "operation_failure \(hint)"
}
}

/// Extract digitization state failure type
private static func extractDigitizationStateFailureType(_ failure: CardNetworkError.DigitizationStateFailure) -> String {
switch failure {
case .configurationFailure:
return "configuration_failure"
case .operationFailure:
return "operation_failure"
}
}

/// Extract digitization state failure type
private static func extractUnableToCopyFailureType(_ failure: CardNetworkError.CopySensitiveDataError) -> String {
switch failure {
case .copyFailure:
return "copy_operation_failure"
case .dataNotViewed:
return "pan_not_viewed"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,18 @@ public extension Card {
switch result {
case .success(let cardDigitizationData):
let digitizationData = DigitizationData.from(cardDigitizationData)
let event = LogEvent.getCardDigitizationState(last4CardID: self?.partIdentifier ?? "",
let event = LogEvent.getCardDigitizationState(cardId: self?.id ?? "",
digitizationState: digitizationData.state)
self?.manager?.logger?.log(event, startedAt: startTime)
completionHandler(.success(digitizationData))
case .failure(let networkError):
self?.manager?.logger?.log(.failure(source: "Get Digitization State", error: networkError), startedAt: startTime)
self?.manager?.logger?.log(
.failure(source: "Get Digitization State",
error: networkError,
networkError: networkError,
additionalInfo: ["cardId": self?.id ?? ""]),
startedAt: startTime
)
completionHandler(.failure(.from(networkError)))
}
}
Expand All @@ -57,11 +63,17 @@ public extension Card {
token: provisioningToken) { [weak self] result in
switch result {
case .success:
let event = LogEvent.pushProvisioning(last4CardID: self?.partIdentifier ?? "")
let event = LogEvent.pushProvisioning(cardId: self?.id ?? "")
self?.manager?.logger?.log(event, startedAt: startTime)
completionHandler(.success)
case .failure(let networkError):
self?.manager?.logger?.log(.failure(source: "Push Provisioning", error: networkError), startedAt: startTime)
self?.manager?.logger?.log(
.failure(
source: "Push Provisioning",
error: networkError,
networkError: networkError,
additionalInfo: ["cardId": self?.id ?? ""]),
startedAt: startTime)
completionHandler(.failure(.from(networkError)))
}
}
Expand Down Expand Up @@ -147,15 +159,20 @@ public extension Card {
completionHandler: @escaping ((CheckoutCardManager.OperationResult) -> Void)) {
switch result {
case .success:
let event = LogEvent.stateManagement(idLast4: partIdentifier,
let event = LogEvent.stateManagement(cardId: id,
originalState: state,
requestedState: newState,
reason: reasonString)
manager?.logger?.log(event, startedAt: startTime)
state = newState
completionHandler(.success)
case .failure(let networkError):
manager?.logger?.log(.failure(source: operationSource, error: networkError), startedAt: startTime)
manager?.logger?.log(
.failure(source: operationSource,
error: networkError,
networkError: networkError,
additionalInfo: ["cardId": id, "originalState": state, "newState": newState, "reason": reasonString ?? ""]),
startedAt: startTime)
completionHandler(.failure(.from(networkError)))
}
}
Expand Down
Loading