diff --git a/Authenticator/Application/AppDelegate.swift b/Authenticator/Application/AppDelegate.swift index 4754f9b03..1ccec1068 100644 --- a/Authenticator/Application/AppDelegate.swift +++ b/Authenticator/Application/AppDelegate.swift @@ -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 diff --git a/Authenticator/Application/Services/ErrorReporter/CrashlyticsErrorReporter.swift b/Authenticator/Application/Services/ErrorReporter/CrashlyticsErrorReporter.swift index 21d3fe040..111cd7fa9 100644 --- a/Authenticator/Application/Services/ErrorReporter/CrashlyticsErrorReporter.swift +++ b/Authenticator/Application/Services/ErrorReporter/CrashlyticsErrorReporter.swift @@ -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 } diff --git a/Bitwarden/Application/AppDelegate.swift b/Bitwarden/Application/AppDelegate.swift index 9c3f65b4b..69fc6ce2a 100644 --- a/Bitwarden/Application/AppDelegate.swift +++ b/Bitwarden/Application/AppDelegate.swift @@ -1,3 +1,4 @@ +import BitwardenKit import BitwardenShared import UIKit diff --git a/Bitwarden/Application/Services/ErrorReporter/CrashlyticsErrorReporter.swift b/Bitwarden/Application/Services/ErrorReporter/CrashlyticsErrorReporter.swift index f7b120b1b..4b6ef6076 100644 --- a/Bitwarden/Application/Services/ErrorReporter/CrashlyticsErrorReporter.swift +++ b/Bitwarden/Application/Services/ErrorReporter/CrashlyticsErrorReporter.swift @@ -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 } diff --git a/BitwardenActionExtension/ActionViewController.swift b/BitwardenActionExtension/ActionViewController.swift index f0ec1b74f..142791c2b 100644 --- a/BitwardenActionExtension/ActionViewController.swift +++ b/BitwardenActionExtension/ActionViewController.swift @@ -1,3 +1,4 @@ +import BitwardenKit import BitwardenShared import MobileCoreServices import UIKit diff --git a/BitwardenAutoFillExtension/CredentialProviderViewController.swift b/BitwardenAutoFillExtension/CredentialProviderViewController.swift index ac0c6049e..e0c9793c4 100644 --- a/BitwardenAutoFillExtension/CredentialProviderViewController.swift +++ b/BitwardenAutoFillExtension/CredentialProviderViewController.swift @@ -1,4 +1,5 @@ import AuthenticationServices +import BitwardenKit import BitwardenSdk import BitwardenShared import Combine diff --git a/BitwardenKit/Core/Platform/Services/ErrorReporter/ErrorReporter.swift b/BitwardenKit/Core/Platform/Services/ErrorReporter/ErrorReporter.swift index 669eebcf3..a639eb27c 100644 --- a/BitwardenKit/Core/Platform/Services/ErrorReporter/ErrorReporter.swift +++ b/BitwardenKit/Core/Platform/Services/ErrorReporter/ErrorReporter.swift @@ -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. diff --git a/BitwardenKit/Core/Platform/Services/ErrorReporter/Mocks/MockErrorReporter.swift b/BitwardenKit/Core/Platform/Services/ErrorReporter/Mocks/MockErrorReporter.swift index a315fb37b..238afb8b3 100644 --- a/BitwardenKit/Core/Platform/Services/ErrorReporter/Mocks/MockErrorReporter.swift +++ b/BitwardenKit/Core/Platform/Services/ErrorReporter/Mocks/MockErrorReporter.swift @@ -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) } diff --git a/BitwardenKit/Core/Platform/Utilities/BitwardenLogger.swift b/BitwardenKit/Core/Platform/Utilities/BitwardenLogger.swift new file mode 100644 index 000000000..d59d5e155 --- /dev/null +++ b/BitwardenKit/Core/Platform/Utilities/BitwardenLogger.swift @@ -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) + } +} diff --git a/AuthenticatorShared/Core/Platform/Utilities/OSLogErrorReporter.swift b/BitwardenKit/Core/Platform/Utilities/OSLogErrorReporter.swift similarity index 72% rename from AuthenticatorShared/Core/Platform/Utilities/OSLogErrorReporter.swift rename to BitwardenKit/Core/Platform/Utilities/OSLogErrorReporter.swift index 5afb2d689..a4b574ed4 100644 --- a/AuthenticatorShared/Core/Platform/Utilities/OSLogErrorReporter.swift +++ b/BitwardenKit/Core/Platform/Utilities/OSLogErrorReporter.swift @@ -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 } diff --git a/BitwardenShareExtension/ShareViewController.swift b/BitwardenShareExtension/ShareViewController.swift index 30fe34aa5..efc34189e 100644 --- a/BitwardenShareExtension/ShareViewController.swift +++ b/BitwardenShareExtension/ShareViewController.swift @@ -1,3 +1,4 @@ +import BitwardenKit import BitwardenShared import Social import UIKit diff --git a/BitwardenShared/Core/Platform/Services/FlightRecorder.swift b/BitwardenShared/Core/Platform/Services/FlightRecorder.swift index 2060d3501..a2c28e3af 100644 --- a/BitwardenShared/Core/Platform/Services/FlightRecorder.swift +++ b/BitwardenShared/Core/Platform/Services/FlightRecorder.swift @@ -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) + } + } +} diff --git a/BitwardenShared/Core/Platform/Services/FlightRecorderTests.swift b/BitwardenShared/Core/Platform/Services/FlightRecorderTests.swift index a7361818f..343fa9fd0 100644 --- a/BitwardenShared/Core/Platform/Services/FlightRecorderTests.swift +++ b/BitwardenShared/Core/Platform/Services/FlightRecorderTests.swift @@ -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)) + } } diff --git a/BitwardenShared/Core/Platform/Services/ServiceContainer.swift b/BitwardenShared/Core/Platform/Services/ServiceContainer.swift index 24c645b5e..dec856466 100644 --- a/BitwardenShared/Core/Platform/Services/ServiceContainer.swift +++ b/BitwardenShared/Core/Platform/Services/ServiceContainer.swift @@ -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, diff --git a/BitwardenShared/Core/Platform/Utilities/OSLogErrorReporter.swift b/BitwardenShared/Core/Platform/Utilities/OSLogErrorReporter.swift deleted file mode 100644 index 5afb2d689..000000000 --- a/BitwardenShared/Core/Platform/Utilities/OSLogErrorReporter.swift +++ /dev/null @@ -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 - } -}