diff --git a/Storage/Storage.xcodeproj/project.pbxproj b/Storage/Storage.xcodeproj/project.pbxproj index 09a5eac84a1..5d398d14f54 100644 --- a/Storage/Storage.xcodeproj/project.pbxproj +++ b/Storage/Storage.xcodeproj/project.pbxproj @@ -17,9 +17,11 @@ 028296F3237D404F00E84012 /* ProductVariation+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028296EF237D404F00E84012 /* ProductVariation+CoreDataProperties.swift */; }; 028296F4237D404F00E84012 /* Attribute+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028296F0237D404F00E84012 /* Attribute+CoreDataClass.swift */; }; 028296F5237D404F00E84012 /* Attribute+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028296F1237D404F00E84012 /* Attribute+CoreDataProperties.swift */; }; + 02A098272480D160002F8C7A /* MockCrashLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A098262480D160002F8C7A /* MockCrashLogger.swift */; }; 02D45649231CFB27008CF0A9 /* StatsVersionBannerVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D45648231CFB27008CF0A9 /* StatsVersionBannerVisibility.swift */; }; 02DA64172313C26400284168 /* StatsVersionBySite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DA64162313C26400284168 /* StatsVersionBySite.swift */; }; 02DA64192313C2AA00284168 /* StatsVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02DA64182313C2AA00284168 /* StatsVersion.swift */; }; + 02EAB6D72480A86D00FD873C /* CrashLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EAB6D62480A86D00FD873C /* CrashLogger.swift */; }; 26577519243D808B003168A5 /* WooCommerceModelV26toV27.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 26577518243D808B003168A5 /* WooCommerceModelV26toV27.xcmappingmodel */; }; 450106892399AC7400E24722 /* TaxClass+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450106882399AC7400E24722 /* TaxClass+CoreDataClass.swift */; }; 4501068B2399AC9B00E24722 /* TaxClass+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4501068A2399AC9B00E24722 /* TaxClass+CoreDataProperties.swift */; }; @@ -156,11 +158,13 @@ 028296F0237D404F00E84012 /* Attribute+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Attribute+CoreDataClass.swift"; sourceTree = ""; }; 028296F1237D404F00E84012 /* Attribute+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Attribute+CoreDataProperties.swift"; sourceTree = ""; }; 028F00652331605000E6C283 /* Model 20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 20.xcdatamodel"; sourceTree = ""; }; + 02A098262480D160002F8C7A /* MockCrashLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCrashLogger.swift; sourceTree = ""; }; 02A9F16A22F9873600EE36EA /* Model 19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 19.xcdatamodel"; sourceTree = ""; }; 02D45648231CFB27008CF0A9 /* StatsVersionBannerVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsVersionBannerVisibility.swift; sourceTree = ""; }; 02DA64162313C26400284168 /* StatsVersionBySite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsVersionBySite.swift; sourceTree = ""; }; 02DA64182313C2AA00284168 /* StatsVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsVersion.swift; sourceTree = ""; }; 02E4F5E223CD552F003B0010 /* Model 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 26.xcdatamodel"; sourceTree = ""; }; + 02EAB6D62480A86D00FD873C /* CrashLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashLogger.swift; sourceTree = ""; }; 262CD20224317C2F00932241 /* Model 27.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 27.xcdatamodel"; sourceTree = ""; }; 26577518243D808B003168A5 /* WooCommerceModelV26toV27.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = WooCommerceModelV26toV27.xcmappingmodel; sourceTree = ""; }; 450106882399AC7400E24722 /* TaxClass+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaxClass+CoreDataClass.swift"; sourceTree = ""; }; @@ -326,6 +330,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 02A098252480D155002F8C7A /* Mocks */ = { + isa = PBXGroup; + children = ( + 02A098262480D160002F8C7A /* MockCrashLogger.swift */, + ); + path = Mocks; + sourceTree = ""; + }; 4DD3D0BDDE216A90FC1335D7 /* Pods */ = { isa = PBXGroup; children = ( @@ -402,6 +414,7 @@ B52B0F7820AA287C00477698 /* StorageManagerType.swift */, B505F6DF20BEEA8100BB1B69 /* StorageType.swift */, D87F61522265AA230031A13B /* FileStorage.swift */, + 02EAB6D62480A86D00FD873C /* CrashLogger.swift */, ); path = Protocols; sourceTree = ""; @@ -454,6 +467,7 @@ isa = PBXGroup; children = ( D87F61582265AED70031A13B /* Mock data */, + 02A098252480D155002F8C7A /* Mocks */, B54CA5C520A4BFC800F38CD1 /* Tools */, B52B0F7D20AA2F1200477698 /* Extensions */, B59E11DC20A9F1E6004121A4 /* CoreData */, @@ -814,6 +828,7 @@ B54CA5BD20A4BD3B00F38CD1 /* NSManagedObjectContext+Storage.swift in Sources */, 933A27302222344D00C2143A /* Logging.swift in Sources */, 747453A52242C85E00E0B5EE /* ProductDefaultAttribute+CoreDataClass.swift in Sources */, + 02EAB6D72480A86D00FD873C /* CrashLogger.swift in Sources */, 746A9D21214078080013F6FF /* TopEarnerStats+CoreDataClass.swift in Sources */, 74B7D6AD20F90CBB002667AC /* OrderNote+CoreDataClass.swift in Sources */, B52B0F7920AA287C00477698 /* StorageManagerType.swift in Sources */, @@ -892,6 +907,7 @@ 9302E3AC220E1CE900DA5018 /* CoreDataIterativeMigratorTests.swift in Sources */, B59E11E020A9F5E6004121A4 /* Constants.swift in Sources */, B54CA5C320A4BF6900F38CD1 /* NSManagedObjectContextStorageTests.swift in Sources */, + 02A098272480D160002F8C7A /* MockCrashLogger.swift in Sources */, B54CA5C720A4BFDC00F38CD1 /* DummyStack.swift in Sources */, B54CA5C220A4BF6900F38CD1 /* NSManagedObjectStorageTests.swift in Sources */, D87F61572265AD980031A13B /* FileStorageTests.swift in Sources */, diff --git a/Storage/Storage/CoreData/CoreDataManager.swift b/Storage/Storage/CoreData/CoreDataManager.swift index f25281d7e27..a1380cf4e2d 100644 --- a/Storage/Storage/CoreData/CoreDataManager.swift +++ b/Storage/Storage/CoreData/CoreDataManager.swift @@ -10,6 +10,7 @@ public class CoreDataManager: StorageManagerType { /// public let name: String + private let crashLogger: CrashLogger /// Designated Initializer. /// @@ -17,8 +18,9 @@ public class CoreDataManager: StorageManagerType { /// /// - Important: This should *match* with your actual Data Model file!. /// - public init(name: String) { + public init(name: String, crashLogger: CrashLogger) { self.name = name + self.crashLogger = crashLogger } @@ -37,11 +39,11 @@ public class CoreDataManager: StorageManagerType { container.persistentStoreDescriptions = [storeDescription] container.loadPersistentStores { [weak self] (storeDescription, error) in - guard let `self` = self, let error = error else { + guard let `self` = self, let persistentStoreLoadingError = error else { return } - DDLogError("⛔️ [CoreDataManager] loadPersistentStore failed. Attempting to recover... \(error)") + DDLogError("⛔️ [CoreDataManager] loadPersistentStore failed. Attempting to recover... \(persistentStoreLoadingError)") /// Backup the old Store /// @@ -49,9 +51,28 @@ public class CoreDataManager: StorageManagerType { let sourceURL = self.storeURL let backupURL = sourceURL.appendingPathExtension("~") try FileManager.default.copyItem(at: sourceURL, to: backupURL) - try FileManager.default.removeItem(at: sourceURL) } catch { - fatalError("☠️ [CoreDataManager] Cannot backup Store: \(error)") + let message = "☠️ [CoreDataManager] Cannot backup Store: \(error)" + self.crashLogger.logMessageAndWait(message, + properties: ["persistentStoreLoadingError": persistentStoreLoadingError, + "backupError": error, + "appState": UIApplication.shared.applicationState.rawValue], + level: .fatal) + fatalError(message) + } + + /// Remove the old Store + /// + do { + try FileManager.default.removeItem(at: self.storeURL) + } catch { + let message = "☠️ [CoreDataManager] Cannot remove Store: \(error)" + self.crashLogger.logMessageAndWait(message, + properties: ["persistentStoreLoadingError": persistentStoreLoadingError, + "removeStoreError": error, + "appState": UIApplication.shared.applicationState.rawValue], + level: .fatal) + fatalError(message) } /// Retry! @@ -61,8 +82,19 @@ public class CoreDataManager: StorageManagerType { return } - fatalError("☠️ [CoreDataManager] Recovery Failed! \(error) [\(error.userInfo)]") + let message = "☠️ [CoreDataManager] Recovery Failed! \(error) [\(error.userInfo)]" + self?.crashLogger.logMessageAndWait(message, + properties: ["persistentStoreLoadingError": persistentStoreLoadingError, + "retryError": error, + "appState": UIApplication.shared.applicationState.rawValue], + level: .fatal) + fatalError(message) } + + self.crashLogger.logMessage("[CoreDataManager] Recovered from persistent store loading error", + properties: ["persistentStoreLoadingError": persistentStoreLoadingError, + "appState": UIApplication.shared.applicationState.rawValue], + level: .info) } return container diff --git a/Storage/Storage/Protocols/CrashLogger.swift b/Storage/Storage/Protocols/CrashLogger.swift new file mode 100644 index 00000000000..5339c2389b7 --- /dev/null +++ b/Storage/Storage/Protocols/CrashLogger.swift @@ -0,0 +1,29 @@ +/// The level of severity, that is currently based on `SentrySeverity`. +public enum SeverityLevel { + case fatal + case error + case warning + case info + case debug +} + +/// Logs crashes or messages at a given severity level. +public protocol CrashLogger { + /** + Writes a message to the Crash Logging system. + - Parameters: + - message: The message + - properties: A dictionary containing additional information about this message + - level: The level of severity to report + */ + func logMessage(_ message: String, properties: [String: Any]?, level: SeverityLevel) + + /** + Writes a message to the Crash Logging system and waits until the message is sent. + - Parameters: + - message: The message + - properties: A dictionary containing additional information about this message + - level: The level of severity to report + */ + func logMessageAndWait(_ message: String, properties: [String: Any]?, level: SeverityLevel) +} diff --git a/Storage/StorageTests/CoreData/CoreDataManagerTests.swift b/Storage/StorageTests/CoreData/CoreDataManagerTests.swift index 20c70b4c5b0..9824751d2ed 100644 --- a/Storage/StorageTests/CoreData/CoreDataManagerTests.swift +++ b/Storage/StorageTests/CoreData/CoreDataManagerTests.swift @@ -10,7 +10,7 @@ class CoreDataManagerTests: XCTestCase { /// Verifies that the Data Model URL contains the ContextIdentifier String. /// func testModelUrlMapsToDataModelWithContextIdentifier() { - let manager = CoreDataManager(name: "WooCommerce") + let manager = CoreDataManager(name: "WooCommerce", crashLogger: MockCrashLogger()) XCTAssertEqual(manager.modelURL.lastPathComponent, "WooCommerce.momd") XCTAssertNoThrow(manager.managedModel) } @@ -18,7 +18,7 @@ class CoreDataManagerTests: XCTestCase { /// Verifies that the Store URL contains the ContextIdentifier string. /// func testStorageUrlMapsToSqliteFileWithContextIdentifier() { - let manager = CoreDataManager(name: "WooCommerce") + let manager = CoreDataManager(name: "WooCommerce", crashLogger: MockCrashLogger()) XCTAssertEqual(manager.storeURL.lastPathComponent, "WooCommerce.sqlite") XCTAssertEqual(manager.storeDescription.url?.lastPathComponent, "WooCommerce.sqlite") } @@ -26,7 +26,7 @@ class CoreDataManagerTests: XCTestCase { /// Verifies that the PersistentContainer properly loads the sqlite database. /// func testPersistentContainerLoadsExpectedDataModelAndSqliteDatabase() { - let manager = CoreDataManager(name: "WooCommerce") + let manager = CoreDataManager(name: "WooCommerce", crashLogger: MockCrashLogger()) let container = manager.persistentContainer XCTAssertEqual(container.managedObjectModel, manager.managedModel) @@ -36,14 +36,14 @@ class CoreDataManagerTests: XCTestCase { /// Verifies that the ContextManager's viewContext matches the PersistenContainer.viewContext /// func testViewContextPropertyReturnsPersistentContainerMainContext() { - let manager = CoreDataManager(name: "WooCommerce") + let manager = CoreDataManager(name: "WooCommerce", crashLogger: MockCrashLogger()) XCTAssertEqual(manager.viewStorage as? NSManagedObjectContext, manager.persistentContainer.viewContext) } /// Verifies that performBackgroundTask effectively runs received closure in BG. /// func testPerformTaskInBackgroundEffectivelyRunsReceivedClosureInBackgroundThread() { - let manager = CoreDataManager(name: "WooCommerce") + let manager = CoreDataManager(name: "WooCommerce", crashLogger: MockCrashLogger()) let expectation = self.expectation(description: "Background") manager.performBackgroundTask { (_) in @@ -57,7 +57,7 @@ class CoreDataManagerTests: XCTestCase { /// Verifies that derived context is instantiated correctly. /// func testDerivedStorageIsInstantiatedCorrectly() { - let manager = CoreDataManager(name: "WooCommerce") + let manager = CoreDataManager(name: "WooCommerce", crashLogger: MockCrashLogger()) let viewContext = (manager.viewStorage as? NSManagedObjectContext) let derivedContext = (manager.newDerivedStorage() as? NSManagedObjectContext) diff --git a/Storage/StorageTests/Mocks/MockCrashLogger.swift b/Storage/StorageTests/Mocks/MockCrashLogger.swift new file mode 100644 index 00000000000..b93d3f830e8 --- /dev/null +++ b/Storage/StorageTests/Mocks/MockCrashLogger.swift @@ -0,0 +1,11 @@ +import Storage + +struct MockCrashLogger: CrashLogger { + func logMessage(_ message: String, properties: [String: Any]?, level: SeverityLevel) { + // no-op + } + + func logMessageAndWait(_ message: String, properties: [String: Any]?, level: SeverityLevel) { + // no-op + } +} diff --git a/WooCommerce/Classes/ServiceLocator/ServiceLocator.swift b/WooCommerce/Classes/ServiceLocator/ServiceLocator.swift index 5465fb21a92..948b107f4fe 100644 --- a/WooCommerce/Classes/ServiceLocator/ServiceLocator.swift +++ b/WooCommerce/Classes/ServiceLocator/ServiceLocator.swift @@ -43,7 +43,7 @@ final class ServiceLocator { /// CoreData Stack /// - private static var _storageManager = CoreDataManager(name: WooConstants.databaseStackName) + private static var _storageManager = CoreDataManager(name: WooConstants.databaseStackName, crashLogger: SentryCrashLogger()) /// Cocoalumberjack DDLog /// diff --git a/WooCommerce/Classes/Tools/Logging/Dictionary+Logging.swift b/WooCommerce/Classes/Tools/Logging/Dictionary+Logging.swift new file mode 100644 index 00000000000..42583171d44 --- /dev/null +++ b/WooCommerce/Classes/Tools/Logging/Dictionary+Logging.swift @@ -0,0 +1,32 @@ +import Foundation + +extension Dictionary where Key == String { + /// Manually serializes a value in a dictionary if the value is not already serializable. + func serializeValuesForLoggingIfNeeded() -> [String: Any] { + guard JSONSerialization.isValidJSONObject(self) == false else { + return self + } + + return reduce([:]) { (properties, entry) -> [String: Any] in + let (key, value) = entry + var formattedProperties: [String: Any] = properties + guard JSONSerialization.isValidJSONObject([key: value]) == false else { + formattedProperties[key] = value + return formattedProperties + } + + if let nsError = value as? NSError { + formattedProperties[key] = [ + "Domain": nsError.domain, + "Code": nsError.code, + "Description": nsError.localizedDescription, + "User Info": nsError.userInfo.description + ] + return formattedProperties + } + + formattedProperties[key] = "\(value)" + return formattedProperties + } + } +} diff --git a/WooCommerce/Classes/Tools/Logging/SentryCrashLogger.swift b/WooCommerce/Classes/Tools/Logging/SentryCrashLogger.swift new file mode 100644 index 00000000000..2f803847368 --- /dev/null +++ b/WooCommerce/Classes/Tools/Logging/SentryCrashLogger.swift @@ -0,0 +1,62 @@ +import AutomatticTracks +import Sentry +import Storage + +/// Logs crashes/messages to Sentry. +final class SentryCrashLogger: CrashLogger { + func logMessage(_ message: String, properties: [String: Any]?, level: SeverityLevel) { + CrashLogging.logMessage(message, properties: properties?.serializeValuesForLoggingIfNeeded(), level: SentrySeverity(level: level)) + } + + func logMessageAndWait(_ message: String, properties: [String: Any]?, level: SeverityLevel) { + CrashLogging.logMessageAndWait(message, properties: properties?.serializeValuesForLoggingIfNeeded(), level: SentrySeverity(level: level)) + } +} + +private extension CrashLogging { + /** + Mostly similar to `logMessage(_:properties:level:)`, but this function blocks the thread until the event is fired. + - Parameters: + - message: The message + - properties: A dictionary containing additional information about this error + - level: The level of severity to report in Sentry + */ + static func logMessageAndWait(_ message: String, properties: [String: Any]?, level: SentrySeverity) { + let event = Event(level: level) + event.message = message + event.extra = properties + + Client.shared?.snapshotStacktrace { + Client.shared?.appendStacktrace(to: event) + } + + guard let client = Client.shared else { + return + } + + let semaphore = DispatchSemaphore(value: 0) + + client.send(event: event) { _ in + semaphore.signal() + } + + semaphore.wait() + } +} + +private extension SentrySeverity { + init(level: SeverityLevel) { + switch level { + case .fatal: + self = .fatal + case .error: + self = .error + case .warning: + self = .warning + case .info: + self = .info + case .debug: + self = .debug + } + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 12f62de712c..c250eae34fe 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -172,6 +172,8 @@ 0286B27C23C7051F003D784B /* ProductImagesViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0286B27823C7051F003D784B /* ProductImagesViewController.xib */; }; 0286B27D23C7051F003D784B /* ProductImagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0286B27923C7051F003D784B /* ProductImagesViewController.swift */; }; 0286B27F23C70557003D784B /* ColumnFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0286B27E23C70557003D784B /* ColumnFlowLayout.swift */; }; + 028AFFB32484ED2800693C09 /* Dictionary+Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028AFFB22484ED2800693C09 /* Dictionary+Logging.swift */; }; + 028AFFB62484EDA000693C09 /* Dictionary+LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028AFFB52484EDA000693C09 /* Dictionary+LoggingTests.swift */; }; 028BAC3D22F2DECE008BB4AF /* StoreStatsAndTopPerformersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028BAC3C22F2DECE008BB4AF /* StoreStatsAndTopPerformersViewController.swift */; }; 028BAC4022F2EFA5008BB4AF /* StoreStatsAndTopPerformersPeriodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028BAC3F22F2EFA5008BB4AF /* StoreStatsAndTopPerformersPeriodViewController.swift */; }; 028BAC4222F30B05008BB4AF /* StoreStatsV4PeriodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028BAC4122F30B05008BB4AF /* StoreStatsV4PeriodViewController.swift */; }; @@ -242,6 +244,7 @@ 02EA6BF82435E80600FFF90A /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EA6BF72435E80600FFF90A /* ImageDownloader.swift */; }; 02EA6BFA2435E92600FFF90A /* KingfisherImageDownloader+ImageDownloadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EA6BF92435E92600FFF90A /* KingfisherImageDownloader+ImageDownloadable.swift */; }; 02EA6BFC2435EC3500FFF90A /* MockImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EA6BFB2435EC3500FFF90A /* MockImageDownloader.swift */; }; + 02EAB6D92480AA4900FD873C /* SentryCrashLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EAB6D82480AA4900FD873C /* SentryCrashLogger.swift */; }; 02EEB5C42424AFAA00B8A701 /* TextFieldTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02EEB5C22424AFAA00B8A701 /* TextFieldTableViewCell.swift */; }; 02EEB5C52424AFAA00B8A701 /* TextFieldTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 02EEB5C32424AFAA00B8A701 /* TextFieldTableViewCell.xib */; }; 02F49ADA23BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F49AD923BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift */; }; @@ -1026,6 +1029,8 @@ 0286B27823C7051F003D784B /* ProductImagesViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ProductImagesViewController.xib; sourceTree = ""; }; 0286B27923C7051F003D784B /* ProductImagesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductImagesViewController.swift; sourceTree = ""; }; 0286B27E23C70557003D784B /* ColumnFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnFlowLayout.swift; sourceTree = ""; }; + 028AFFB22484ED2800693C09 /* Dictionary+Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Logging.swift"; sourceTree = ""; }; + 028AFFB52484EDA000693C09 /* Dictionary+LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+LoggingTests.swift"; sourceTree = ""; }; 028BAC3C22F2DECE008BB4AF /* StoreStatsAndTopPerformersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsAndTopPerformersViewController.swift; sourceTree = ""; }; 028BAC3F22F2EFA5008BB4AF /* StoreStatsAndTopPerformersPeriodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsAndTopPerformersPeriodViewController.swift; sourceTree = ""; }; 028BAC4122F30B05008BB4AF /* StoreStatsV4PeriodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreStatsV4PeriodViewController.swift; sourceTree = ""; }; @@ -1096,6 +1101,7 @@ 02EA6BF72435E80600FFF90A /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = ""; }; 02EA6BF92435E92600FFF90A /* KingfisherImageDownloader+ImageDownloadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KingfisherImageDownloader+ImageDownloadable.swift"; sourceTree = ""; }; 02EA6BFB2435EC3500FFF90A /* MockImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageDownloader.swift; sourceTree = ""; }; + 02EAB6D82480AA4900FD873C /* SentryCrashLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashLogger.swift; sourceTree = ""; }; 02EEB5C22424AFAA00B8A701 /* TextFieldTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewCell.swift; sourceTree = ""; }; 02EEB5C32424AFAA00B8A701 /* TextFieldTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TextFieldTableViewCell.xib; sourceTree = ""; }; 02F49AD923BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TitleAndTextFieldTableViewCell.ViewModel+State.swift"; sourceTree = ""; }; @@ -2130,6 +2136,14 @@ path = Media; sourceTree = ""; }; + 028AFFB42484ED7F00693C09 /* Logging */ = { + isa = PBXGroup; + children = ( + 028AFFB52484EDA000693C09 /* Dictionary+LoggingTests.swift */, + ); + path = Logging; + sourceTree = ""; + }; 028BAC4322F3AE3B008BB4AF /* Stats v4 */ = { isa = PBXGroup; children = ( @@ -2740,6 +2754,7 @@ B53A569521123D27000776C9 /* Tools */ = { isa = PBXGroup; children = ( + 028AFFB42484ED7F00693C09 /* Logging */, 5798191124526FC7000817F8 /* Observables */, D8A8C4F22268288F001C72BF /* AddManualCustomTrackingViewModelTests.swift */, D83F5938225B424B00626E75 /* AddManualTrackingViewModelTests.swift */, @@ -3102,6 +3117,8 @@ children = ( 933A27362222354600C2143A /* Logging.swift */, B5DB01B42114AB2D00A4F797 /* CrashLogging.swift */, + 02EAB6D82480AA4900FD873C /* SentryCrashLogger.swift */, + 028AFFB22484ED2800693C09 /* Dictionary+Logging.swift */, ); path = Logging; sourceTree = ""; @@ -4683,6 +4700,7 @@ 02162729237965E8000208D2 /* ProductFormTableViewModel.swift in Sources */, CE2A9FBF23BFB1BE002BEC1C /* LedgerTableViewCell.swift in Sources */, B58B4AC02108FF6100076FDD /* Array+Helpers.swift in Sources */, + 028AFFB32484ED2800693C09 /* Dictionary+Logging.swift in Sources */, B5A56BF0219F2CE90065A902 /* VerticalButton.swift in Sources */, 748C7780211E18A600814F2C /* OrderStats+Woo.swift in Sources */, D831E2DC230E0558000037D0 /* Authentication.swift in Sources */, @@ -4748,6 +4766,7 @@ B517EA1D218B41F200730EC4 /* String+Woo.swift in Sources */, 02564A8C246CE38E00D6DB2A /* SwappableSubviewContainerView.swift in Sources */, 024DF31223742B18006658FE /* AztecItalicFormatBarCommand.swift in Sources */, + 02EAB6D92480AA4900FD873C /* SentryCrashLogger.swift in Sources */, D83F5933225B2EB900626E75 /* ManualTrackingViewController.swift in Sources */, B57C744A20F5649300EEFC87 /* EmptyStoresTableViewCell.swift in Sources */, 027D67D1245ADDF40036B8DB /* FilterTypeViewModel+Helpers.swift in Sources */, @@ -5094,6 +5113,7 @@ CECC759923D6160000486676 /* AggregateDataHelperTests.swift in Sources */, 020BE76F23B4A468007FE54C /* AztecBlockquoteFormatBarCommandTests.swift in Sources */, 748C7784211E2D8400814F2C /* DoubleWooTests.swift in Sources */, + 028AFFB62484EDA000693C09 /* Dictionary+LoggingTests.swift in Sources */, 02A275BE23FE57DC005C560F /* ProductUIImageLoaderTests.swift in Sources */, 027B8BBF23FE0F850040944E /* MockMediaStoresManager.swift in Sources */, 453770D12431FF4700AC718D /* ProductSettingsViewModelTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Tools/Logging/Dictionary+LoggingTests.swift b/WooCommerce/WooCommerceTests/Tools/Logging/Dictionary+LoggingTests.swift new file mode 100644 index 00000000000..f8be1a96e50 --- /dev/null +++ b/WooCommerce/WooCommerceTests/Tools/Logging/Dictionary+LoggingTests.swift @@ -0,0 +1,48 @@ +import XCTest +@testable import WooCommerce + +final class Dictionary_LoggingTests: XCTestCase { + func testSerializingValuesForAnEmptyDictionary() { + // Arrange + let dictionary: [String: Any] = [:] + + // Action + let serializableDictionary = dictionary.serializeValuesForLoggingIfNeeded() + + // Assert + XCTAssertTrue(JSONSerialization.isValidJSONObject(serializableDictionary)) + XCTAssertEqual(serializableDictionary.count, 0) + } + + func testSerializingValuesForADictionaryWithNSErrors() { + // Arrange + let error = NSError(domain: "Testing crash logging in Sentry on Reviews tab launch", + code: 100, + userInfo: ["reason": "Testing only"]) + let error1Key = "Error 1" + let error2Key = "Error 2" + let dictionary: [String: Any] = [error1Key: error, error2Key: error, "message": "hello"] + + // Action + let serializableDictionary = dictionary.serializeValuesForLoggingIfNeeded() + + // Assert + XCTAssertTrue(JSONSerialization.isValidJSONObject(serializableDictionary)) + XCTAssertEqual(serializableDictionary.count, 3) + } + + func testSerializingValuesForADictionaryWithUnsupportedType() { + // Arrange + struct Value { + let message: String + } + let dictionary: [String: Any] = ["test": Value(message: "😃")] + + // Action + let serializableDictionary = dictionary.serializeValuesForLoggingIfNeeded() + + // Assert + XCTAssertTrue(JSONSerialization.isValidJSONObject(serializableDictionary)) + XCTAssertEqual(serializableDictionary.count, 1) + } +} diff --git a/Yosemite/Yosemite.xcodeproj/project.pbxproj b/Yosemite/Yosemite.xcodeproj/project.pbxproj index ddb3af97380..1df6c63975d 100644 --- a/Yosemite/Yosemite.xcodeproj/project.pbxproj +++ b/Yosemite/Yosemite.xcodeproj/project.pbxproj @@ -47,6 +47,7 @@ 026D52C0238235930092AE05 /* ProductVariationStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026D52BF238235930092AE05 /* ProductVariationStoreTests.swift */; }; 028BCE2422DE22BB00056966 /* SiteVisitStatsStoreErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 028BCE2322DE22BB00056966 /* SiteVisitStatsStoreErrorTests.swift */; }; 029B00A7230D64E800B0AE66 /* StatsTimeRangeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 029B00A6230D64E800B0AE66 /* StatsTimeRangeTests.swift */; }; + 02A098242480D0D8002F8C7A /* MockCrashLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02A098232480D0D8002F8C7A /* MockCrashLogger.swift */; }; 02BA23C222EEEABC009539E7 /* AvailabilityStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BA23C122EEEABC009539E7 /* AvailabilityStore.swift */; }; 02BA23C422EEEB3B009539E7 /* AvailabilityAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BA23C322EEEB3B009539E7 /* AvailabilityAction.swift */; }; 02BA23C622EEF092009539E7 /* StatsV4AvailabilityStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BA23C522EEF092009539E7 /* StatsV4AvailabilityStoreTests.swift */; }; @@ -280,6 +281,7 @@ 026D52BF238235930092AE05 /* ProductVariationStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductVariationStoreTests.swift; sourceTree = ""; }; 028BCE2322DE22BB00056966 /* SiteVisitStatsStoreErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteVisitStatsStoreErrorTests.swift; sourceTree = ""; }; 029B00A6230D64E800B0AE66 /* StatsTimeRangeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTimeRangeTests.swift; sourceTree = ""; }; + 02A098232480D0D8002F8C7A /* MockCrashLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCrashLogger.swift; sourceTree = ""; }; 02BA23C122EEEABC009539E7 /* AvailabilityStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailabilityStore.swift; sourceTree = ""; }; 02BA23C322EEEB3B009539E7 /* AvailabilityAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvailabilityAction.swift; sourceTree = ""; }; 02BA23C522EEF092009539E7 /* StatsV4AvailabilityStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsV4AvailabilityStoreTests.swift; sourceTree = ""; }; @@ -991,6 +993,7 @@ 0202B6962387AFBF00F3EBE0 /* MockInMemoryStorage.swift */, 020220E32396969E00290165 /* MockProduct.swift */, 0248B36A2459127200A271A4 /* MockupNetwork+Path.swift */, + 02A098232480D0D8002F8C7A /* MockCrashLogger.swift */, ); path = Mockups; sourceTree = ""; @@ -1382,6 +1385,7 @@ 029B00A7230D64E800B0AE66 /* StatsTimeRangeTests.swift in Sources */, 02E493ED245C0EBC000AEA9E /* Product+SettingsTests.swift in Sources */, 0248B36924590FC300A271A4 /* ProductStore+FilterProductsTests.swift in Sources */, + 02A098242480D0D8002F8C7A /* MockCrashLogger.swift in Sources */, 02FF055F23D985710058E6E7 /* URL+MediaTests.swift in Sources */, 02124DAC24318D6B00980D74 /* Media+MediaTypeTests.swift in Sources */, 025CA2D0238F54E800B05C81 /* ProductShippingClassStoreTests.swift in Sources */, diff --git a/Yosemite/YosemiteTests/Mockups/MockCrashLogger.swift b/Yosemite/YosemiteTests/Mockups/MockCrashLogger.swift new file mode 100644 index 00000000000..b93d3f830e8 --- /dev/null +++ b/Yosemite/YosemiteTests/Mockups/MockCrashLogger.swift @@ -0,0 +1,11 @@ +import Storage + +struct MockCrashLogger: CrashLogger { + func logMessage(_ message: String, properties: [String: Any]?, level: SeverityLevel) { + // no-op + } + + func logMessageAndWait(_ message: String, properties: [String: Any]?, level: SeverityLevel) { + // no-op + } +} diff --git a/Yosemite/YosemiteTests/Tools/ShippingSettings/StorageShippingSettingsServiceTests.swift b/Yosemite/YosemiteTests/Tools/ShippingSettings/StorageShippingSettingsServiceTests.swift index 40c8f02ab7c..8a5dee023a6 100644 --- a/Yosemite/YosemiteTests/Tools/ShippingSettings/StorageShippingSettingsServiceTests.swift +++ b/Yosemite/YosemiteTests/Tools/ShippingSettings/StorageShippingSettingsServiceTests.swift @@ -10,7 +10,12 @@ final class StorageShippingSettingsServiceTests: XCTestCase { override func setUp() { super.setUp() - storage = CoreDataManager(name: "WooCommerce") + storage = CoreDataManager(name: "WooCommerce", crashLogger: MockCrashLogger()) + } + + override func tearDown() { + storage = nil + super.tearDown() } func testInitialShippingSettings() {