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..4ae81d55834 100644 --- a/Storage/Storage/CoreData/CoreDataManager.swift +++ b/Storage/Storage/CoreData/CoreDataManager.swift @@ -10,15 +10,18 @@ public class CoreDataManager: StorageManagerType { /// public let name: String + private let crashLogger: CrashLogger /// Designated Initializer. /// /// - Parameter name: Identifier to be used for: [database, data model, container]. + /// - Parameter crashLogger: allows logging a message of any severity level /// /// - 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 +40,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 +52,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 +83,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/Resources/AppStoreStrings.pot b/WooCommerce/Resources/AppStoreStrings.pot index ef48c3d1408..853404d0eb3 100644 --- a/WooCommerce/Resources/AppStoreStrings.pot +++ b/WooCommerce/Resources/AppStoreStrings.pot @@ -47,12 +47,20 @@ msgctxt "app_store_promo_text" msgid "Run your store wherever you are. The WooCommerce app makes it easy to manage orders and inventory, track sales, and monitor store activity like new orders and reviews." msgstr "" -msgctxt "v4.3-whats-new" +msgctxt "v4.4-whats-new" msgid "" -"* You can now turn Products M2 Features on and off for simple products in Settings > Experimental Features. Products M2 features the ability to update product images, product short description, product viewing, sharing, and settings (Product settings include status, visibility, catalog visibility, slug, purchase note, and menu order.)\n" -"* You can now edit and save details on the Order details or Top Performers tabs. We’ve also improved accessibility with better VoiceOver support for Product Price settings.\n" -"* We’ve made some visual refinements: In Order Details, the Payment card now appears right after the Products and Refunded Products cards. The WIP banner on the Products tab is now collapsed by default. Yay for more vertical space!\n" -"* We’ve dropped support for iOS 11 and lower.\n" +"We’re continuing to add functionality to product editing. Look out for improvements to image and product management — head to “Settings” in the My Store tab to learn more and turn on the new features!\n" +"\n" +"Push notifications and update badges work better together. The Reviews tab no longer shows a badge after you receive a new order push notification, and navigating to the Orders and/or the Reviews tab now clears the badge number (depending on the types of notifications).\n" +"\n" +"Nudges are a bit less nudge-y. When editing products, the “discard changes” prompt now only appears if you’ve deleted images, and not when you’re leaving product settings detail screens with a text field (slug, purchase note, and menu order).\n" +"\n" +"We also straightened out a few little UI foibles:\n" +"- The product name displays in the product details navigation bar, so it’s always visible.\n" +"- We fixed an issue that prevented the product details from scrolling all the way to the bottom in landscape mode after dismissing the keyboard.\n" +"- Images pending upload will be visible after editing product images from product details.\n" +"- The “View product in store” action displays only if the product is published.\n" +"- HTML codes in the shipping method are now shown correctly.\n" msgstr "" #. translators: This is a promo message that will be attached on top of a screenshot in the App Store. diff --git a/WooCommerce/Resources/release_notes.txt b/WooCommerce/Resources/release_notes.txt index 6a734c25df4..c9ca2221410 100644 --- a/WooCommerce/Resources/release_notes.txt +++ b/WooCommerce/Resources/release_notes.txt @@ -1,4 +1,12 @@ -* You can now turn Products M2 Features on and off for simple products in Settings > Experimental Features. Products M2 features the ability to update product images, product short description, product viewing, sharing, and settings (Product settings include status, visibility, catalog visibility, slug, purchase note, and menu order.) -* You can now edit and save details on the Order details or Top Performers tabs. We’ve also improved accessibility with better VoiceOver support for Product Price settings. -* We’ve made some visual refinements: In Order Details, the Payment card now appears right after the Products and Refunded Products cards. The WIP banner on the Products tab is now collapsed by default. Yay for more vertical space! -* We’ve dropped support for iOS 11 and lower. +We’re continuing to add functionality to product editing. Look out for improvements to image and product management — head to “Settings” in the My Store tab to learn more and turn on the new features! + +Push notifications and update badges work better together. The Reviews tab no longer shows a badge after you receive a new order push notification, and navigating to the Orders and/or the Reviews tab now clears the badge number (depending on the types of notifications). + +Nudges are a bit less nudge-y. When editing products, the “discard changes” prompt now only appears if you’ve deleted images, and not when you’re leaving product settings detail screens with a text field (slug, purchase note, and menu order). + +We also straightened out a few little UI foibles: +- The product name displays in the product details navigation bar, so it’s always visible. +- We fixed an issue that prevented the product details from scrolling all the way to the bottom in landscape mode after dismissing the keyboard. +- Images pending upload will be visible after editing product images from product details. +- The “View product in store” action displays only if the product is published. +- HTML codes in the shipping method are now shown correctly. diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index 4995ab23ca2..6da93a44523 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -175,6 +175,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 */; }; @@ -245,6 +247,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 */; }; @@ -1031,6 +1034,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 = ""; }; @@ -1101,6 +1106,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 = ""; }; @@ -2137,6 +2143,14 @@ path = Media; sourceTree = ""; }; + 028AFFB42484ED7F00693C09 /* Logging */ = { + isa = PBXGroup; + children = ( + 028AFFB52484EDA000693C09 /* Dictionary+LoggingTests.swift */, + ); + path = Logging; + sourceTree = ""; + }; 028BAC4322F3AE3B008BB4AF /* Stats v4 */ = { isa = PBXGroup; children = ( @@ -2747,6 +2761,7 @@ B53A569521123D27000776C9 /* Tools */ = { isa = PBXGroup; children = ( + 028AFFB42484ED7F00693C09 /* Logging */, 5798191124526FC7000817F8 /* Observables */, D8A8C4F22268288F001C72BF /* AddManualCustomTrackingViewModelTests.swift */, D83F5938225B424B00626E75 /* AddManualTrackingViewModelTests.swift */, @@ -3109,6 +3124,8 @@ children = ( 933A27362222354600C2143A /* Logging.swift */, B5DB01B42114AB2D00A4F797 /* CrashLogging.swift */, + 02EAB6D82480AA4900FD873C /* SentryCrashLogger.swift */, + 028AFFB22484ED2800693C09 /* Dictionary+Logging.swift */, ); path = Logging; sourceTree = ""; @@ -4688,6 +4705,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 */, @@ -4754,6 +4772,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 */, @@ -5100,6 +5119,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 */, 025A1246247CDF55008EA761 /* ProductFormViewModel+ChangesTests.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 16785f16ded..a286efb9c3b 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 */; }; @@ -281,6 +282,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 = ""; }; @@ -1002,6 +1004,7 @@ 0202B6962387AFBF00F3EBE0 /* MockInMemoryStorage.swift */, 020220E32396969E00290165 /* MockProduct.swift */, 0248B36A2459127200A271A4 /* MockupNetwork+Path.swift */, + 02A098232480D0D8002F8C7A /* MockCrashLogger.swift */, ); path = Mockups; sourceTree = ""; @@ -1393,6 +1396,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/Storage/StorageManagerConcurrencyTests.swift b/Yosemite/YosemiteTests/Storage/StorageManagerConcurrencyTests.swift index 00418d36498..2cbb885f0fc 100644 --- a/Yosemite/YosemiteTests/Storage/StorageManagerConcurrencyTests.swift +++ b/Yosemite/YosemiteTests/Storage/StorageManagerConcurrencyTests.swift @@ -63,7 +63,7 @@ final class StorageManagerConcurrencyTests: XCTestCase { override func setUp() { super.setUp() // Use the Sqlite-based StorageManagerType to be closer to the production operations - storageManager = CoreDataManager(name: "WooCommerce") + storageManager = CoreDataManager(name: "WooCommerce", crashLogger: MockCrashLogger()) storageManager.reset() } 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() {