Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge Release/4.4 back into develop #2378

Merged
merged 19 commits into from
Jun 4, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Storage/Storage.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -156,11 +158,13 @@
028296F0237D404F00E84012 /* Attribute+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Attribute+CoreDataClass.swift"; sourceTree = "<group>"; };
028296F1237D404F00E84012 /* Attribute+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Attribute+CoreDataProperties.swift"; sourceTree = "<group>"; };
028F00652331605000E6C283 /* Model 20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 20.xcdatamodel"; sourceTree = "<group>"; };
02A098262480D160002F8C7A /* MockCrashLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCrashLogger.swift; sourceTree = "<group>"; };
02A9F16A22F9873600EE36EA /* Model 19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 19.xcdatamodel"; sourceTree = "<group>"; };
02D45648231CFB27008CF0A9 /* StatsVersionBannerVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsVersionBannerVisibility.swift; sourceTree = "<group>"; };
02DA64162313C26400284168 /* StatsVersionBySite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsVersionBySite.swift; sourceTree = "<group>"; };
02DA64182313C2AA00284168 /* StatsVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsVersion.swift; sourceTree = "<group>"; };
02E4F5E223CD552F003B0010 /* Model 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 26.xcdatamodel"; sourceTree = "<group>"; };
02EAB6D62480A86D00FD873C /* CrashLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashLogger.swift; sourceTree = "<group>"; };
262CD20224317C2F00932241 /* Model 27.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Model 27.xcdatamodel"; sourceTree = "<group>"; };
26577518243D808B003168A5 /* WooCommerceModelV26toV27.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = WooCommerceModelV26toV27.xcmappingmodel; sourceTree = "<group>"; };
450106882399AC7400E24722 /* TaxClass+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaxClass+CoreDataClass.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -326,6 +330,14 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
02A098252480D155002F8C7A /* Mocks */ = {
isa = PBXGroup;
children = (
02A098262480D160002F8C7A /* MockCrashLogger.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
4DD3D0BDDE216A90FC1335D7 /* Pods */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -402,6 +414,7 @@
B52B0F7820AA287C00477698 /* StorageManagerType.swift */,
B505F6DF20BEEA8100BB1B69 /* StorageType.swift */,
D87F61522265AA230031A13B /* FileStorage.swift */,
02EAB6D62480A86D00FD873C /* CrashLogger.swift */,
);
path = Protocols;
sourceTree = "<group>";
Expand Down Expand Up @@ -454,6 +467,7 @@
isa = PBXGroup;
children = (
D87F61582265AED70031A13B /* Mock data */,
02A098252480D155002F8C7A /* Mocks */,
B54CA5C520A4BFC800F38CD1 /* Tools */,
B52B0F7D20AA2F1200477698 /* Extensions */,
B59E11DC20A9F1E6004121A4 /* CoreData */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
44 changes: 38 additions & 6 deletions Storage/Storage/CoreData/CoreDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ 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].
///
/// - 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
}


Expand All @@ -37,21 +39,40 @@ 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
///
do {
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!
Expand All @@ -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
Expand Down
29 changes: 29 additions & 0 deletions Storage/Storage/Protocols/CrashLogger.swift
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 6 additions & 6 deletions Storage/StorageTests/CoreData/CoreDataManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ 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)
}

/// 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")
}

/// 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)
Expand All @@ -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
Expand All @@ -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)

Expand Down
11 changes: 11 additions & 0 deletions Storage/StorageTests/Mocks/MockCrashLogger.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion WooCommerce/Classes/ServiceLocator/ServiceLocator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down
32 changes: 32 additions & 0 deletions WooCommerce/Classes/Tools/Logging/Dictionary+Logging.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
62 changes: 62 additions & 0 deletions WooCommerce/Classes/Tools/Logging/SentryCrashLogger.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading