mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-10 00:42:29 -06:00
[PM-26063] Add Flight Recorder logging for API requests and navigation in Authenticator (#2150)
This commit is contained in:
parent
5bf7e24f97
commit
7c901c2944
@ -30,10 +30,12 @@ class APIService {
|
||||
/// - client: The underlying `HTTPClient` that performs the network request. Defaults
|
||||
/// to `URLSession.shared`.
|
||||
/// - environmentService: The service used by the application to retrieve the environment settings.
|
||||
/// - flightRecorder: The service used by the application for recording temporary debug logs.
|
||||
///
|
||||
init(
|
||||
client: HTTPClient = URLSession.shared,
|
||||
environmentService: EnvironmentService,
|
||||
flightRecorder: FlightRecorder,
|
||||
) {
|
||||
self.client = client
|
||||
|
||||
@ -47,6 +49,9 @@ class APIService {
|
||||
apiUnauthenticatedService = HTTPService(
|
||||
baseURLGetter: { environmentService.apiURL },
|
||||
client: client,
|
||||
loggers: [
|
||||
FlightRecorderHTTPLogger(flightRecorder: flightRecorder),
|
||||
],
|
||||
requestHandlers: [defaultHeadersRequestHandler],
|
||||
responseHandlers: [responseValidationHandler],
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import TestHelpers
|
||||
import XCTest
|
||||
|
||||
@ -31,4 +32,19 @@ class APIServiceTests: BitwardenTestCase {
|
||||
)
|
||||
XCTAssertNil(subject.apiUnauthenticatedService.tokenProvider)
|
||||
}
|
||||
|
||||
/// `init(client:)` configures the API service to use `FlightRecorderHTTPLogger` for logging
|
||||
/// API requests.
|
||||
func test_init_configuresFlightRecorderLogger() {
|
||||
let mockFlightRecorder = MockFlightRecorder()
|
||||
let subject = APIService(
|
||||
client: MockHTTPClient(),
|
||||
environmentService: MockEnvironmentService(),
|
||||
flightRecorder: mockFlightRecorder,
|
||||
)
|
||||
|
||||
XCTAssertTrue(
|
||||
subject.apiUnauthenticatedService.loggers.contains(where: { $0 is FlightRecorderHTTPLogger }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,12 @@ import Networking
|
||||
extension APIService {
|
||||
convenience init(
|
||||
client: HTTPClient,
|
||||
flightRecorder: FlightRecorder = MockFlightRecorder(),
|
||||
) {
|
||||
self.init(
|
||||
client: client,
|
||||
environmentService: MockEnvironmentService(),
|
||||
flightRecorder: flightRecorder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,6 +188,7 @@ public class ServiceContainer: Services {
|
||||
let cameraService = DefaultCameraService()
|
||||
let dataStore = DataStore(errorReporter: errorReporter)
|
||||
let keychainService = DefaultKeychainService()
|
||||
let timeProvider = CurrentTime()
|
||||
|
||||
let keychainRepository = DefaultKeychainRepository(
|
||||
appIdService: appIdService,
|
||||
@ -199,10 +200,19 @@ public class ServiceContainer: Services {
|
||||
dataStore: dataStore,
|
||||
)
|
||||
|
||||
let flightRecorder = DefaultFlightRecorder(
|
||||
appInfoService: appInfoService,
|
||||
errorReporter: errorReporter,
|
||||
stateService: stateService,
|
||||
timeProvider: timeProvider,
|
||||
)
|
||||
errorReporter.add(logger: flightRecorder)
|
||||
|
||||
let environmentService = DefaultEnvironmentService()
|
||||
|
||||
let apiService = APIService(
|
||||
environmentService: environmentService,
|
||||
flightRecorder: flightRecorder,
|
||||
)
|
||||
|
||||
let errorReportBuilder = DefaultErrorReportBuilder(
|
||||
@ -210,7 +220,6 @@ public class ServiceContainer: Services {
|
||||
appInfoService: appInfoService,
|
||||
)
|
||||
|
||||
let timeProvider = CurrentTime()
|
||||
let totpExpirationManagerFactory = DefaultTOTPExpirationManagerFactory(timeProvider: timeProvider)
|
||||
|
||||
let biometricsRepository = DefaultBiometricsRepository(
|
||||
@ -243,14 +252,6 @@ public class ServiceContainer: Services {
|
||||
cryptographyKeyService: cryptographyKeyService,
|
||||
)
|
||||
|
||||
let flightRecorder = DefaultFlightRecorder(
|
||||
appInfoService: appInfoService,
|
||||
errorReporter: errorReporter,
|
||||
stateService: stateService,
|
||||
timeProvider: timeProvider,
|
||||
)
|
||||
errorReporter.add(logger: flightRecorder)
|
||||
|
||||
let migrationService = DefaultMigrationService(
|
||||
appSettingsStore: appSettingsStore,
|
||||
errorReporter: errorReporter,
|
||||
|
||||
@ -14,6 +14,7 @@ class AppCoordinator: Coordinator, HasRootNavigator {
|
||||
typealias Module = AuthModule
|
||||
& DebugMenuModule
|
||||
& ItemListModule
|
||||
& NavigatorBuilderModule
|
||||
& TabModule
|
||||
& TutorialModule
|
||||
|
||||
@ -107,7 +108,7 @@ class AppCoordinator: Coordinator, HasRootNavigator {
|
||||
coordinator.navigate(to: authRoute)
|
||||
} else {
|
||||
guard let rootNavigator else { return }
|
||||
let navigationController = UINavigationController()
|
||||
let navigationController = module.makeNavigationController()
|
||||
let coordinator = module.makeAuthCoordinator(
|
||||
delegate: self,
|
||||
rootNavigator: rootNavigator,
|
||||
@ -148,7 +149,7 @@ class AppCoordinator: Coordinator, HasRootNavigator {
|
||||
/// Shows the welcome tutorial.
|
||||
///
|
||||
private func showTutorial() {
|
||||
let navigationController = UINavigationController()
|
||||
let navigationController = module.makeNavigationController()
|
||||
let coordinator = module.makeTutorialCoordinator(
|
||||
stackNavigator: navigationController,
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import BitwardenKit
|
||||
import UIKit
|
||||
|
||||
// MARK: AppModule
|
||||
|
||||
@ -70,3 +71,11 @@ extension DefaultAppModule: FlightRecorderModule {
|
||||
.asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DefaultAppModule + NavigatorBuilderModule
|
||||
|
||||
extension DefaultAppModule: NavigatorBuilderModule {
|
||||
public func makeNavigationController() -> UINavigationController {
|
||||
ViewLoggingNavigationController(logger: services.flightRecorder)
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,6 +12,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
/// The module types required by this coordinator for creating child coordinators.
|
||||
typealias Module = FileSelectionModule
|
||||
& FlightRecorderModule
|
||||
& NavigatorBuilderModule
|
||||
& TutorialModule
|
||||
|
||||
typealias Services = HasAppInfoService
|
||||
@ -133,8 +134,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
services: services,
|
||||
)
|
||||
let view = ExportItemsView(store: Store(processor: processor))
|
||||
let navController = UINavigationController(rootViewController: UIHostingController(rootView: view))
|
||||
stackNavigator?.present(navController)
|
||||
stackNavigator?.present(view)
|
||||
}
|
||||
|
||||
/// Shows a flight recorder view.
|
||||
@ -156,8 +156,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
services: services,
|
||||
)
|
||||
let view = ImportItemsView(store: Store(processor: processor))
|
||||
let navController = UINavigationController(rootViewController: UIHostingController(rootView: view))
|
||||
stackNavigator?.present(navController)
|
||||
stackNavigator?.present(view)
|
||||
}
|
||||
|
||||
/// Presents an activity controller for importing items.
|
||||
@ -174,7 +173,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
}
|
||||
|
||||
private func showImportItemsQrCode(delegate: AuthenticatorKeyCaptureDelegate) async {
|
||||
let navigationController = UINavigationController()
|
||||
let navigationController = module.makeNavigationController()
|
||||
let coordinator = AuthenticatorKeyCaptureCoordinator(
|
||||
delegate: delegate,
|
||||
services: services,
|
||||
@ -197,8 +196,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
state: SelectLanguageState(currentLanguage: currentLanguage),
|
||||
)
|
||||
let view = SelectLanguageView(store: Store(processor: processor))
|
||||
let navController = UINavigationController(rootViewController: UIHostingController(rootView: view))
|
||||
stackNavigator?.present(navController)
|
||||
stackNavigator?.present(view)
|
||||
}
|
||||
|
||||
/// Shows the settings screen.
|
||||
@ -216,7 +214,7 @@ final class SettingsCoordinator: Coordinator, HasStackNavigator {
|
||||
/// Shows the welcome tutorial.
|
||||
///
|
||||
private func showTutorial() {
|
||||
let navigationController = UINavigationController()
|
||||
let navigationController = module.makeNavigationController()
|
||||
let coordinator = module.makeTutorialCoordinator(
|
||||
stackNavigator: navigationController,
|
||||
)
|
||||
|
||||
@ -64,9 +64,10 @@ class SettingsCoordinatorTests: BitwardenTestCase {
|
||||
func test_navigateTo_exportVault() throws {
|
||||
subject.navigate(to: .exportItems)
|
||||
|
||||
let navigationController = try XCTUnwrap(stackNavigator.actions.last?.view as? UINavigationController)
|
||||
XCTAssertTrue(stackNavigator.actions.last?.view is UINavigationController)
|
||||
XCTAssertTrue(navigationController.viewControllers.first is UIHostingController<ExportItemsView>)
|
||||
let action = try XCTUnwrap(stackNavigator.actions.last)
|
||||
XCTAssertEqual(action.embedInNavigationController, true)
|
||||
XCTAssertEqual(action.type, .presented)
|
||||
XCTAssertTrue(action.view is ExportItemsView)
|
||||
}
|
||||
|
||||
/// `navigate(to:)` with `.flightRecorder` starts flight recorder coordinator and navigates to
|
||||
@ -84,9 +85,10 @@ class SettingsCoordinatorTests: BitwardenTestCase {
|
||||
func test_navigateTo_selectLanguage() throws {
|
||||
subject.navigate(to: .selectLanguage(currentLanguage: .default))
|
||||
|
||||
let navigationController = try XCTUnwrap(stackNavigator.actions.last?.view as? UINavigationController)
|
||||
XCTAssertTrue(stackNavigator.actions.last?.view is UINavigationController)
|
||||
XCTAssertTrue(navigationController.viewControllers.first is UIHostingController<SelectLanguageView>)
|
||||
let action = try XCTUnwrap(stackNavigator.actions.last)
|
||||
XCTAssertEqual(action.embedInNavigationController, true)
|
||||
XCTAssertEqual(action.type, .presented)
|
||||
XCTAssertTrue(action.view is SelectLanguageView)
|
||||
}
|
||||
|
||||
/// `navigate(to:)` with `.settings` pushes the settings view onto the stack navigator.
|
||||
|
||||
@ -11,6 +11,7 @@ final class TabCoordinator: Coordinator, HasTabNavigator {
|
||||
|
||||
/// The module types required by this coordinator for creating child coordinators.
|
||||
typealias Module = ItemListModule
|
||||
& NavigatorBuilderModule
|
||||
& SettingsModule
|
||||
|
||||
// MARK: Properties
|
||||
@ -85,13 +86,13 @@ final class TabCoordinator: Coordinator, HasTabNavigator {
|
||||
|
||||
rootNavigator.show(child: tabNavigator)
|
||||
|
||||
let itemListNavigator = UINavigationController()
|
||||
let itemListNavigator = module.makeNavigationController()
|
||||
itemListNavigator.navigationBar.prefersLargeTitles = true
|
||||
itemListCoordinator = module.makeItemListCoordinator(
|
||||
stackNavigator: itemListNavigator,
|
||||
)
|
||||
|
||||
let settingsNavigator = UINavigationController()
|
||||
let settingsNavigator = module.makeNavigationController()
|
||||
settingsNavigator.navigationBar.prefersLargeTitles = true
|
||||
let settingsCoordinator = module.makeSettingsCoordinator(
|
||||
stackNavigator: settingsNavigator,
|
||||
|
||||
@ -10,6 +10,7 @@ class AuthenticatorItemCoordinator: NSObject, Coordinator, HasStackNavigator {
|
||||
// MARK: Types
|
||||
|
||||
typealias Module = AuthenticatorItemModule
|
||||
& NavigatorBuilderModule
|
||||
|
||||
typealias Services = HasAuthenticatorItemRepository
|
||||
& HasErrorAlertServices.ErrorAlertServices
|
||||
@ -81,7 +82,7 @@ class AuthenticatorItemCoordinator: NSObject, Coordinator, HasStackNavigator {
|
||||
/// - Parameter route: The route to navigate to in the presented coordinator.
|
||||
///
|
||||
private func presentChildAuthenticatorItemCoordinator(route: AuthenticatorItemRoute, context: AnyObject?) {
|
||||
let navigationController = UINavigationController()
|
||||
let navigationController = module.makeNavigationController()
|
||||
let coordinator = module.makeAuthenticatorItemCoordinator(stackNavigator: navigationController)
|
||||
coordinator.navigate(to: route, context: context)
|
||||
coordinator.start()
|
||||
|
||||
@ -11,6 +11,7 @@ final class ItemListCoordinator: Coordinator, HasStackNavigator {
|
||||
|
||||
typealias Module = AuthenticatorItemModule
|
||||
& ItemListModule
|
||||
& NavigatorBuilderModule
|
||||
|
||||
typealias Services = HasTimeProvider
|
||||
& HasErrorAlertServices.ErrorAlertServices
|
||||
@ -80,7 +81,7 @@ final class ItemListCoordinator: Coordinator, HasStackNavigator {
|
||||
/// Shows the totp camera setup screen.
|
||||
///
|
||||
private func showCamera(delegate: AuthenticatorKeyCaptureDelegate) async {
|
||||
let navigationController = UINavigationController()
|
||||
let navigationController = module.makeNavigationController()
|
||||
let coordinator = AuthenticatorKeyCaptureCoordinator(
|
||||
delegate: delegate,
|
||||
services: services,
|
||||
@ -95,7 +96,7 @@ final class ItemListCoordinator: Coordinator, HasStackNavigator {
|
||||
/// Shows the totp manual setup screen.
|
||||
///
|
||||
private func showManualTotp(delegate: AuthenticatorKeyCaptureDelegate) {
|
||||
let navigationController = UINavigationController()
|
||||
let navigationController = module.makeNavigationController()
|
||||
let coordinator = AuthenticatorKeyCaptureCoordinator(
|
||||
delegate: delegate,
|
||||
services: services,
|
||||
@ -127,7 +128,7 @@ final class ItemListCoordinator: Coordinator, HasStackNavigator {
|
||||
/// - Parameter route: The route to navigate to in the coordinator.
|
||||
///
|
||||
private func showItem(route: AuthenticatorItemRoute, delegate: AuthenticatorItemOperationDelegate? = nil) {
|
||||
let navigationController = UINavigationController()
|
||||
let navigationController = module.makeNavigationController()
|
||||
let coordinator = module.makeAuthenticatorItemCoordinator(stackNavigator: navigationController)
|
||||
coordinator.start()
|
||||
coordinator.navigate(to: route, context: delegate)
|
||||
|
||||
@ -12,9 +12,3 @@ public protocol NavigatorBuilderModule: AnyObject {
|
||||
///
|
||||
func makeNavigationController() -> UINavigationController
|
||||
}
|
||||
|
||||
extension DefaultAppModule: NavigatorBuilderModule {
|
||||
public func makeNavigationController() -> UINavigationController {
|
||||
ViewLoggingNavigationController(logger: services.flightRecorder)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
import BitwardenKit
|
||||
import UIKit
|
||||
|
||||
// MARK: - ViewLoggingNavigationController
|
||||
@ -10,7 +9,7 @@ import UIKit
|
||||
/// `UINavigationController` *or* the `delegate` and `presentationController?.delegate` need to be
|
||||
/// passed from an existing navigation controller to any newly created `UINavigationController`.
|
||||
///
|
||||
class ViewLoggingNavigationController: UINavigationController,
|
||||
public class ViewLoggingNavigationController: UINavigationController,
|
||||
UINavigationControllerDelegate,
|
||||
UIAdaptivePresentationControllerDelegate {
|
||||
// MARK: Properties
|
||||
@ -24,7 +23,7 @@ class ViewLoggingNavigationController: UINavigationController,
|
||||
///
|
||||
/// - Parameter logger: The logger instance used to log when views appear and are dismissed.
|
||||
///
|
||||
init(logger: BitwardenLogger) {
|
||||
public init(logger: BitwardenLogger) {
|
||||
self.logger = logger
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
@ -36,7 +35,7 @@ class ViewLoggingNavigationController: UINavigationController,
|
||||
|
||||
// MARK: View Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
delegate = self
|
||||
presentationController?.delegate = self
|
||||
@ -44,14 +43,14 @@ class ViewLoggingNavigationController: UINavigationController,
|
||||
|
||||
// MARK: UINavigationController
|
||||
|
||||
override func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
override public func dismiss(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
super.dismiss(animated: animated, completion: completion)
|
||||
logger.log("[Navigation] View dismissed: \(resolveLoggingViewName(for: self))")
|
||||
}
|
||||
|
||||
// MARK: UINavigationControllerDelegate
|
||||
|
||||
func navigationController(
|
||||
public func navigationController(
|
||||
_ navigationController: UINavigationController,
|
||||
didShow viewController: UIViewController,
|
||||
animated: Bool,
|
||||
@ -61,7 +60,7 @@ class ViewLoggingNavigationController: UINavigationController,
|
||||
|
||||
// MARK: UIAdaptivePresentationControllerDelegate
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
let viewController = presentationController.presentedViewController
|
||||
logger.log("[Navigation] View dismissed interactively: \(resolveLoggingViewName(for: viewController))")
|
||||
}
|
||||
@ -2,7 +2,7 @@ import BitwardenKitMocks
|
||||
import SwiftUI
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
@testable import BitwardenKit
|
||||
|
||||
class ViewLoggingNavigationControllerTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
@ -1,4 +1,5 @@
|
||||
import BitwardenKit
|
||||
import UIKit
|
||||
|
||||
// MARK: AppModule
|
||||
|
||||
@ -76,3 +77,11 @@ extension DefaultAppModule: FlightRecorderModule {
|
||||
.asAnyCoordinator()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DefaultAppModule + NavigatorBuilderModule
|
||||
|
||||
extension DefaultAppModule: NavigatorBuilderModule {
|
||||
public func makeNavigationController() -> UINavigationController {
|
||||
ViewLoggingNavigationController(logger: services.flightRecorder)
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,6 +115,13 @@ class AppModuleTests: BitwardenTestCase {
|
||||
XCTAssertTrue(navigationController.viewControllers[0] is UIHostingController<ImportLoginsView>)
|
||||
}
|
||||
|
||||
/// `makeNavigationController()` builds a navigation controller.
|
||||
@MainActor
|
||||
func test_makeNavigationController() {
|
||||
let navigationController = subject.makeNavigationController()
|
||||
XCTAssertTrue(navigationController is ViewLoggingNavigationController)
|
||||
}
|
||||
|
||||
/// `makePasswordAutoFillCoordinator` builds the password autofill coordinator.
|
||||
@MainActor
|
||||
func test_makePasswordAutoFillCoordinator() {
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import XCTest
|
||||
|
||||
@testable import BitwardenShared
|
||||
|
||||
class NavigatorBuilderModuleTests: BitwardenTestCase {
|
||||
// MARK: Properties
|
||||
|
||||
var subject: DefaultAppModule!
|
||||
|
||||
// MARK: Setup & Teardown
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
|
||||
subject = DefaultAppModule(services: .withMocks())
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
super.tearDown()
|
||||
|
||||
subject = nil
|
||||
}
|
||||
|
||||
// MARK: Tests
|
||||
|
||||
/// `makeNavigationController()` builds a navigation controller.
|
||||
@MainActor
|
||||
func test_makeNavigationController() {
|
||||
let navigationController = subject.makeNavigationController()
|
||||
XCTAssertTrue(navigationController is ViewLoggingNavigationController)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
@testable import AuthenticatorShared
|
||||
import BitwardenKit
|
||||
import BitwardenKitMocks
|
||||
import UIKit
|
||||
|
||||
// MARK: - MockAppModule
|
||||
|
||||
@ -11,6 +12,7 @@ class MockAppModule:
|
||||
FileSelectionModule,
|
||||
FlightRecorderModule,
|
||||
ItemListModule,
|
||||
NavigatorBuilderModule,
|
||||
TutorialModule,
|
||||
TabModule {
|
||||
var appCoordinator = MockCoordinator<AppRoute, AppEvent>()
|
||||
@ -69,6 +71,10 @@ class MockAppModule:
|
||||
itemListCoordinator.asAnyCoordinator()
|
||||
}
|
||||
|
||||
func makeNavigationController() -> UINavigationController {
|
||||
UINavigationController()
|
||||
}
|
||||
|
||||
func makeTabCoordinator(
|
||||
errorReporter _: ErrorReporter,
|
||||
rootNavigator _: RootNavigator,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user