diff --git a/MailCore/API/SyncedAuthenticator.swift b/MailCore/API/SyncedAuthenticator.swift index 65bd005f0..4e266dfc7 100644 --- a/MailCore/API/SyncedAuthenticator.swift +++ b/MailCore/API/SyncedAuthenticator.swift @@ -23,34 +23,63 @@ import InfomaniakDI import InfomaniakLogin import Sentry +public extension ApiToken { + var isInfinite: Bool { + expirationDate == nil + } + + var metadata: [String: Any] { + [ + "User id": userId, + "Expiration date": expirationDate?.timeIntervalSince1970 ?? "infinite", + "Access Token": truncatedAccessToken, + "Refresh Token": truncatedRefreshToken + ] + } +} + final class SyncedAuthenticator: OAuthAuthenticator { @LazyInjectService var keychainHelper: KeychainHelper @LazyInjectService var tokenStore: TokenStore @LazyInjectService var networkLoginService: InfomaniakNetworkLoginable - func handleFailedRefreshingToken(oldToken: ApiToken, error: Error?) -> Result { + func handleFailedRefreshingToken(oldToken: ApiToken, + newToken: ApiToken?, + error: Error?) -> Result { guard let error else { // Couldn't refresh the token, keep the old token and fetch it later. Maybe because of bad network ? - SentrySDK - .addBreadcrumb(oldToken.generateBreadcrumb(level: .error, - message: "Refreshing token failed - Other \(error.debugDescription)")) + Log.tokenAuthentication( + "Refreshing token failed - Other \(error.debugDescription)", + oldToken: oldToken, + newToken: newToken, + level: .error + ) + return .failure(MailError.unknownError) } if case .noRefreshToken = (error as? InfomaniakLoginError) { // Couldn't refresh the token because we don't have a refresh token - SentrySDK - .addBreadcrumb(oldToken.generateBreadcrumb(level: .error, - message: "Refreshing token failed - Cannot refresh infinite token")) + Log.tokenAuthentication( + "Refreshing token failed - Cannot refresh infinite token", + oldToken: oldToken, + newToken: newToken, + level: .error + ) + refreshTokenDelegate?.didFailRefreshToken(oldToken) return .failure(error) } if (error as NSError).domain == "invalid_grant" { // Couldn't refresh the token, API says it's invalid - SentrySDK - .addBreadcrumb(oldToken.generateBreadcrumb(level: .error, - message: "Refreshing token failed - Invalid grant")) + Log.tokenAuthentication( + "Refreshing token failed - Invalid grant", + oldToken: oldToken, + newToken: newToken, + level: .error + ) + refreshTokenDelegate?.didFailRefreshToken(oldToken) return .failure(error) } @@ -64,23 +93,37 @@ final class SyncedAuthenticator: OAuthAuthenticator { for session: Session, completion: @escaping (Result) -> Void ) { - SentrySDK.addBreadcrumb((credential as ApiToken).generateBreadcrumb(level: .info, message: "Refreshing token - Starting")) + let storedToken = tokenStore.tokenFor(userId: credential.userId, fetchLocation: .keychain) + + Log.tokenAuthentication( + "Refreshing token - Starting", + oldToken: storedToken, + newToken: credential, + level: .info + ) guard keychainHelper.isKeychainAccessible else { - SentrySDK - .addBreadcrumb((credential as ApiToken) - .generateBreadcrumb(level: .error, message: "Refreshing token failed - Keychain unaccessible")) + Log.tokenAuthentication( + "Refreshing token failed - Keychain unaccessible", + oldToken: storedToken, + newToken: credential, + level: .error + ) + completion(.failure(MailError.keychainUnavailable)) return } - if let storedToken = tokenStore.tokenFor(userId: credential.userId, fetchLocation: .keychain) { + if let storedToken { // Someone else refreshed our token and we already have an infinite token if storedToken.expirationDate == nil && credential.expirationDate != nil { - SentrySDK.addBreadcrumb(storedToken.generateBreadcrumb( - level: .info, - message: "Refreshing token - Success with local (infinite)" - )) + Log.tokenAuthentication( + "Refreshing token - Success with local (infinite)", + oldToken: storedToken, + newToken: credential, + level: .info + ) + completion(.success(storedToken)) return } @@ -88,10 +131,13 @@ final class SyncedAuthenticator: OAuthAuthenticator { if let storedTokenExpirationDate = storedToken.expirationDate, let tokenExpirationDate = credential.expirationDate, tokenExpirationDate > storedTokenExpirationDate { - SentrySDK.addBreadcrumb(storedToken.generateBreadcrumb( - level: .info, - message: "Refreshing token - Success with local" - )) + Log.tokenAuthentication( + "Refreshing token - Success with local", + oldToken: storedToken, + newToken: credential, + level: .info + ) + completion(.success(storedToken)) return } @@ -103,13 +149,17 @@ final class SyncedAuthenticator: OAuthAuthenticator { networkLoginService.refreshToken(token: credential) { token, error in // New token has been fetched correctly if let token { - SentrySDK - .addBreadcrumb(token - .generateBreadcrumb(level: .info, message: "Refreshing token - Success with remote")) + Log.tokenAuthentication( + "Refreshing token - Success with remote", + oldToken: credential, + newToken: token, + level: .info + ) + self.refreshTokenDelegate?.didUpdateToken(newToken: token, oldToken: credential) completion(.success(token)) } else { - completion(self.handleFailedRefreshingToken(oldToken: credential, error: error)) + completion(self.handleFailedRefreshingToken(oldToken: credential, newToken: token, error: error)) } expiringActivity.endAll() } diff --git a/MailCore/Utils/Logging.swift b/MailCore/Utils/Logging.swift index 9934dc824..2b1ffe076 100644 --- a/MailCore/Utils/Logging.swift +++ b/MailCore/Utils/Logging.swift @@ -22,6 +22,7 @@ import CocoaLumberjackSwift import Foundation import InfomaniakCore import InfomaniakDI +import InfomaniakLogin import RealmSwift import Sentry @@ -125,3 +126,41 @@ public enum Logging { #endif } } + +/// Something to centralize log methods per feature +public enum Log { + public static func tokenAuthentication(_ message: @autoclosure () -> String, + oldToken: ApiToken?, + newToken: ApiToken?, + level: SentryLevel = .debug, + file: StaticString = #file, + function: StaticString = #function, + line: UInt = #line) { + let message = message() + let oldTokenMetadata: Any = oldToken?.metadata ?? "NULL" + let newTokenMetadata: Any = newToken?.metadata ?? "NULL" + var metadata = [String: Any]() + metadata["oldToken"] = oldTokenMetadata + metadata["newToken"] = newTokenMetadata + + SentryDebug.asyncCapture( + message: message, + context: metadata, + level: level, + extras: ["file": "\(file)", "function": "\(function)", "line": "\(line)"] + ) + + SentryDebug.addAsyncBreadcrumb(level: level, + category: SentryDebug.Category.threadAlgorithm.rawValue, + message: message, + data: metadata) + + if level == .error { + DDLogError(message) + DDLogError("old token: \(oldTokenMetadata)") + DDLogError("new token: \(newTokenMetadata)") + } else { + DDLogInfo(message) + } + } +} diff --git a/MailCore/Utils/SentryDebug.swift b/MailCore/Utils/SentryDebug.swift index fd963dba6..805a33b52 100644 --- a/MailCore/Utils/SentryDebug.swift +++ b/MailCore/Utils/SentryDebug.swift @@ -174,8 +174,9 @@ public enum SentryDebug { // MARK: - Breadcrumb - enum Category { - static let ThreadAlgorithm = "Thread algo" + public enum Category: String { + case threadAlgorithm = "Thread algo" + case syncedAuthenticator = "SyncedAuthenticator" } private static func createBreadcrumb(level: SentryLevel, @@ -189,19 +190,9 @@ public enum SentryDebug { return crumb } - private static func addAsyncBreadcrumb(level: SentryLevel, - category: String, - message: String, - data: [String: Any]? = nil) { - Task { - let breadcrumb = createBreadcrumb(level: level, category: category, message: message, data: data) - SentrySDK.addBreadcrumb(breadcrumb) - } - } - static func nilDateParsingBreadcrumb(uid: String) { let breadcrumb = createBreadcrumb(level: .warning, - category: Category.ThreadAlgorithm, + category: Category.threadAlgorithm.rawValue, message: "Nil message date decoded", data: ["uid": uid]) SentrySDK.addBreadcrumb(breadcrumb) @@ -343,3 +334,62 @@ public enum SentryDebug { return true } } + +// MARK: - SHARED - + +public extension SentryDebug { + static func addAsyncBreadcrumb(level: SentryLevel, + category: String, + message: String, + data: [String: Any]? = nil) { + Task { + let breadcrumb = Breadcrumb(level: level, category: category) + breadcrumb.message = message + breadcrumb.data = data + SentrySDK.addBreadcrumb(breadcrumb) + } + } + + static func asyncCapture( + error: Error, + context: [String: Any]? = nil, + contextKey: String? = nil, + extras: [String: Any]? = nil + ) { + Task { + SentrySDK.capture(error: error) { scope in + if let context, let contextKey { + scope.setContext(value: context, key: contextKey) + } + + if let extras { + scope.setExtras(extras) + } + } + } + } + + static func asyncCapture( + message: String, + context: [String: Any]? = nil, + contextKey: String? = nil, + level: SentryLevel? = nil, + extras: [String: Any]? = nil + ) { + Task { + SentrySDK.capture(message: message) { scope in + if let context, let contextKey { + scope.setContext(value: context, key: contextKey) + } + + if let level { + scope.setLevel(level) + } + + if let extras { + scope.setExtras(extras) + } + } + } + } +}