[PM-19577] Log ErrorReporter errors to the flight recorder (#1547)

This commit is contained in:
Matt Czech 2025-05-06 16:02:17 -05:00 committed by GitHub
parent 129973cad5
commit c6993cd212
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 106 additions and 42 deletions

View File

@ -1,4 +1,5 @@
import AuthenticatorShared
import BitwardenKit
import UIKit
/// A protocol for an `AppDelegate` that can be used by the `SceneDelegate` to look up the

View File

@ -6,6 +6,11 @@ import FirebaseCrashlytics
/// An `ErrorReporter` that logs non-fatal errors to Crashlytics for investigation.
///
final class CrashlyticsErrorReporter: ErrorReporter {
// MARK: Properties
/// A list of additional loggers that errors will be logged to.
private var additionalLoggers: [any BitwardenLogger] = []
// MARK: ErrorReporter Properties
var isEnabled: Bool {
@ -25,7 +30,16 @@ final class CrashlyticsErrorReporter: ErrorReporter {
// MARK: ErrorReporter
public func add(logger: any BitwardenLogger) {
additionalLoggers.append(logger)
}
func log(error: Error) {
let callStack = Thread.callStackSymbols.joined(separator: "\n")
for logger in additionalLoggers {
logger.log("Error: \(error)\n\(callStack)")
}
// Don't log networking related errors to Crashlytics.
guard !error.isNetworkingError else { return }

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenShared
import UIKit

View File

@ -6,6 +6,11 @@ import FirebaseCrashlytics
/// An `ErrorReporter` that logs non-fatal errors to Crashlytics for investigation.
///
final class CrashlyticsErrorReporter: ErrorReporter {
// MARK: Properties
/// A list of additional loggers that errors will be logged to.
private var additionalLoggers: [any BitwardenLogger] = []
// MARK: ErrorReporter Properties
var isEnabled: Bool {
@ -25,7 +30,16 @@ final class CrashlyticsErrorReporter: ErrorReporter {
// MARK: ErrorReporter
public func add(logger: any BitwardenLogger) {
additionalLoggers.append(logger)
}
func log(error: Error) {
let callStack = Thread.callStackSymbols.joined(separator: "\n")
for logger in additionalLoggers {
logger.log("Error: \(error)\n\(callStack)")
}
// Don't log networking related errors to Crashlytics.
guard !error.isNetworkingError else { return }

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenShared
import MobileCoreServices
import UIKit

View File

@ -1,4 +1,5 @@
import AuthenticationServices
import BitwardenKit
import BitwardenSdk
import BitwardenShared
import Combine

View File

@ -8,6 +8,12 @@ public protocol ErrorReporter: AnyObject {
// MARK: Methods
/// Add an additional logger that will any errors will be logged to.
///
/// - Parameter logger: The additional logger that any errors will be logged to.
///
func add(logger: BitwardenLogger)
/// Logs an error to be reported.
///
/// - Parameter error: The error to log.

View File

@ -1,6 +1,7 @@
@testable import BitwardenKit
public class MockErrorReporter: ErrorReporter {
public var additionalLoggers = [any BitwardenLogger]()
public var currentUserId: String?
public var errors = [Error]()
public var isEnabled = false
@ -8,6 +9,10 @@ public class MockErrorReporter: ErrorReporter {
public init() {}
public func add(logger: any BitwardenLogger) {
additionalLoggers.append(logger)
}
public func log(error: Error) {
errors.append(error)
}

View File

@ -0,0 +1,25 @@
/// A protocol for an object that handles logging app messages.
///
public protocol BitwardenLogger {
/// Logs a message.
///
/// - Parameters:
/// - message: The message to log.
/// - file: The file that called the log method.
/// - line: The line number in the file that called the log method.
///
func log(_ message: String, file: String, line: UInt)
}
public extension BitwardenLogger {
/// Logs a message.
///
/// - Parameters:
/// - message: The message to log.
/// - file: The file that called the log method.
/// - line: The line number in the file that called the log method.
///
func log(_ message: String, file: String = #file, line: UInt = #line) {
log(message, file: file, line: line)
}
}

View File

@ -1,4 +1,3 @@
import BitwardenKit
import OSLog
/// An `ErrorReporter` that logs non-fatal errors to the console via OSLog.
@ -6,6 +5,9 @@ import OSLog
public final class OSLogErrorReporter: ErrorReporter {
// MARK: Properties
/// A list of additional loggers that errors will be logged to.
private var additionalLoggers: [any BitwardenLogger] = []
/// The logger instance to log local messages.
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ErrorReporter")
@ -21,9 +23,18 @@ public final class OSLogErrorReporter: ErrorReporter {
// MARK: ErrorReporter
public func add(logger: any BitwardenLogger) {
additionalLoggers.append(logger)
}
public func log(error: Error) {
logger.error("Error: \(error)")
let callStack = Thread.callStackSymbols.joined(separator: "\n")
for logger in additionalLoggers {
logger.log("Error: \(error as NSError)\n\(callStack)")
}
// Don't crash for networking related errors.
guard !error.isNetworkingError else { return }

View File

@ -1,3 +1,4 @@
import BitwardenKit
import BitwardenShared
import Social
import UIKit

View File

@ -424,3 +424,13 @@ extension DefaultFlightRecorder: FlightRecorder {
}
}
}
// MARK: DefaultFlightRecorder + BitwardenLogger
extension DefaultFlightRecorder: BitwardenLogger {
nonisolated func log(_ message: String, file: String, line: UInt) {
Task {
await log(message, file: file, line: line)
}
}
}

View File

@ -598,4 +598,18 @@ class FlightRecorderTests: BitwardenTestCase { // swiftlint:disable:this type_bo
await subject.log("Hello world!")
XCTAssertNil(stateService.flightRecorderData)
}
// MARK: DefaultFlightRecorder Tests
/// `DefaultFlightRecorder` implements `BitwardenLogger.log()` which logs to the active log.
func test_log_bitwardenLogger() throws {
stateService.flightRecorderData = FlightRecorderData(activeLog: activeLog)
(subject as? DefaultFlightRecorder)?.log("Hello world!")
waitFor { self.fileManager.appendDataData != nil }
let appendedMessage = try String(data: XCTUnwrap(fileManager.appendDataData), encoding: .utf8)
XCTAssertEqual(appendedMessage, "2025-01-01T00:00:00Z: Hello world!\n")
XCTAssertEqual(stateService.flightRecorderData, FlightRecorderData(activeLog: activeLog))
}
}

View File

@ -398,6 +398,7 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
stateService: stateService,
timeProvider: timeProvider
)
errorReporter.add(logger: flightRecorder)
let rehydrationHelper = DefaultRehydrationHelper(
errorReporter: errorReporter,

View File

@ -1,41 +0,0 @@
import BitwardenKit
import OSLog
/// An `ErrorReporter` that logs non-fatal errors to the console via OSLog.
///
public final class OSLogErrorReporter: ErrorReporter {
// MARK: Properties
/// The logger instance to log local messages.
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ErrorReporter")
// MARK: ErrorReporter Properties
/// This exists here satisfy the `ErrorReporter` protocol, but doesn't do anything since we
/// don't report these errors to an external service.
public var isEnabled = true
// MARK: Initialization
public init() {}
// MARK: ErrorReporter
public func log(error: Error) {
logger.error("Error: \(error)")
// Don't crash for networking related errors.
guard !error.isNetworkingError else { return }
// Crash in debug builds to make the error more visible during development.
assertionFailure("Unexpected error: \(error)")
}
public func setRegion(_ region: String, isPreAuth: Bool) {
// No-op
}
public func setUserId(_ userId: String?) {
// No-op
}
}