@logbrew/react-native 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LogBrewReactNative.podspec +25 -3
  2. package/README.md +174 -4
  3. package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +611 -0
  4. package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +129 -0
  5. package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
  6. package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
  7. package/apple-native-diagnostics.d.ts +52 -0
  8. package/apple-native-diagnostics.js +286 -0
  9. package/expo.cjs +46 -0
  10. package/expo.d.ts +16 -0
  11. package/expo.js +5 -0
  12. package/fatal-replay.cjs +12 -1
  13. package/global-errors.cjs +3 -1
  14. package/index.cjs +86 -2
  15. package/index.d.cts +7 -0
  16. package/index.d.ts +7 -0
  17. package/index.js +86 -2
  18. package/index.native.d.ts +47 -1
  19. package/index.native.js +81 -1
  20. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
  21. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
  22. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
  23. package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
  29. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
  30. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
  31. package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
  32. package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
  33. package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
  34. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
  35. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
  36. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
  37. package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
  38. package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
  39. package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
  40. package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
  41. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
  42. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
  43. package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
  44. package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
  45. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
  46. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
  47. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
  48. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
  49. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
  50. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
  51. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
  52. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
  53. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
  54. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
  55. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
  56. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
  57. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
  58. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
  59. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
  60. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
  61. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
  62. package/ios/LBRNEventRecordStore.h +29 -0
  63. package/ios/LBRNEventRecordStore.m +746 -0
  64. package/ios/LBRNFatalStoreModule.mm +159 -14
  65. package/ios/LBRNPrivateStorage.h +7 -0
  66. package/ios/LBRNPrivateStorage.m +38 -0
  67. package/package.json +47 -4
  68. package/persistent-delivery.native.js +282 -0
  69. package/promise-rejections.cjs +3 -1
  70. package/scripts/sync-apple-native-sources.mjs +93 -0
  71. package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
  72. package/src/NativeLogBrewFatalStore.ts +9 -0
@@ -0,0 +1,125 @@
1
+ // Generated by scripts/sync-apple-native-sources.mjs.
2
+ // Edit the canonical Swift source, then regenerate this package boundary.
3
+ import Foundation
4
+ @preconcurrency import KSCrash
5
+
6
+ enum CrashMonitor: Equatable, Hashable {
7
+ case machException
8
+ case signal
9
+ case cppException
10
+ case objectiveCException
11
+ }
12
+
13
+ struct CrashEngineConfiguration: Equatable {
14
+ let storageDirectory: URL
15
+ let maxStoredReports: Int
16
+ let monitors: Set<CrashMonitor>
17
+ let includesMemory: Bool
18
+ let includesQueueNames: Bool
19
+ let includesConsoleLog: Bool
20
+ let artifactIdentity: NativeArtifactIdentityValue?
21
+ let deletionIsExplicit: Bool
22
+ }
23
+
24
+ protocol CrashEngineDriving: AnyObject {
25
+ func install(configuration: CrashEngineConfiguration) throws -> any CrashReportStoring
26
+ }
27
+
28
+ protocol CrashReportStoring: AnyObject {
29
+ var reportIDs: [Int64] { get }
30
+ func report(for id: Int64) -> [String: Any]?
31
+ func deleteReport(with id: Int64)
32
+ func deleteAllReports()
33
+ }
34
+
35
+ final class ProcessCrashCaptureOwnership: @unchecked Sendable {
36
+ static let shared = ProcessCrashCaptureOwnership()
37
+
38
+ private let lock = NSLock()
39
+ private weak var owner: AnyObject?
40
+ private var claimed = false
41
+
42
+ func claim(_ candidate: AnyObject) throws {
43
+ lock.lock()
44
+ defer { lock.unlock() }
45
+ if let owner {
46
+ guard owner === candidate else {
47
+ throw NativeCrashError(.ownershipConflict)
48
+ }
49
+ return
50
+ }
51
+ guard !claimed else {
52
+ throw NativeCrashError(.ownershipConflict)
53
+ }
54
+ owner = candidate
55
+ claimed = true
56
+ }
57
+ }
58
+
59
+ final class KSCrashEngineDriver: CrashEngineDriving {
60
+ func install(configuration: CrashEngineConfiguration) throws -> any CrashReportStoring {
61
+ let storeConfiguration = CrashReportStoreConfiguration()
62
+ storeConfiguration.appName = "logbrew-native-crash"
63
+ storeConfiguration.maxReportCount = configuration.maxStoredReports
64
+ storeConfiguration.reportCleanupPolicy = configuration.deletionIsExplicit ? .never : .always
65
+
66
+ let engineConfiguration = KSCrashConfiguration()
67
+ engineConfiguration.installPath = configuration.storageDirectory.path
68
+ engineConfiguration.reportStoreConfiguration = storeConfiguration
69
+ var monitors: MonitorType = []
70
+ if configuration.monitors.contains(.machException) {
71
+ monitors.insert(.machException)
72
+ }
73
+ if configuration.monitors.contains(.signal) {
74
+ monitors.insert(.signal)
75
+ }
76
+ if configuration.monitors.contains(.cppException) {
77
+ monitors.insert(.cppException)
78
+ }
79
+ if configuration.monitors.contains(.objectiveCException) {
80
+ monitors.insert(.nsException)
81
+ }
82
+ engineConfiguration.monitors = monitors
83
+ engineConfiguration.userInfoJSON = configuration.artifactIdentity?.crashUserInfoJSON
84
+ engineConfiguration.deadlockWatchdogInterval = 0
85
+ engineConfiguration.enableQueueNameSearch = configuration.includesQueueNames
86
+ engineConfiguration.enableMemoryIntrospection = configuration.includesMemory
87
+ engineConfiguration.addConsoleLogToReport = configuration.includesConsoleLog
88
+ engineConfiguration.printPreviousLogOnStartup = false
89
+ engineConfiguration.enableSigTermMonitoring = false
90
+
91
+ do {
92
+ try KSCrash.shared.install(with: engineConfiguration)
93
+ } catch {
94
+ throw NativeCrashError(.engineInstallFailed)
95
+ }
96
+ guard let reportStore = KSCrash.shared.reportStore else {
97
+ throw NativeCrashError(.engineInstallFailed)
98
+ }
99
+ return KSCrashReportStoreAdapter(store: reportStore)
100
+ }
101
+ }
102
+
103
+ private final class KSCrashReportStoreAdapter: CrashReportStoring {
104
+ private let store: CrashReportStore
105
+
106
+ init(store: CrashReportStore) {
107
+ self.store = store
108
+ }
109
+
110
+ var reportIDs: [Int64] {
111
+ store.reportIDs.map(\.int64Value)
112
+ }
113
+
114
+ func report(for id: Int64) -> [String: Any]? {
115
+ store.report(for: id)?.value
116
+ }
117
+
118
+ func deleteReport(with id: Int64) {
119
+ store.deleteReport(with: id)
120
+ }
121
+
122
+ func deleteAllReports() {
123
+ store.deleteAllReports()
124
+ }
125
+ }
@@ -0,0 +1,139 @@
1
+ // Generated by scripts/sync-apple-native-sources.mjs.
2
+ // Edit the canonical Swift source, then regenerate this package boundary.
3
+ import CryptoKit
4
+ import Foundation
5
+
6
+ struct CrashReportSanitizer {
7
+ let maxReplayBytes: Int
8
+ let ownerNonce: UUID
9
+
10
+ func makeRecord(reportID: Int64, rawReport: [String: Any]) throws -> NativeCrashRecord {
11
+ let data = try canonicalData(rawReport)
12
+ guard let report = rawReport["report"] as? [String: Any],
13
+ let rawID = report["id"] as? String,
14
+ let uuid = UUID(uuidString: rawID),
15
+ let rawTimestamp = report["timestamp"] as? String,
16
+ let timestamp = NativeCrashTimestamp(rawTimestamp)
17
+ else {
18
+ throw NativeCrashError(.reportCorrupt)
19
+ }
20
+
21
+ let crash = rawReport["crash"] as? [String: Any]
22
+ let error = crash?["error"] as? [String: Any]
23
+ let nativeStackFrames = NativeStackFrameSanitizer().frames(from: rawReport)
24
+ let artifactIdentity = try NativeArtifactIdentityValue.persistedIdentity(in: rawReport)
25
+ return NativeCrashRecord(
26
+ eventID: uuid.uuidString.lowercased(),
27
+ timestamp: timestamp.normalized,
28
+ mechanism: mechanism(error?["type"] as? String),
29
+ nativeStackFrames: nativeStackFrames,
30
+ artifactIdentity: artifactIdentity,
31
+ hangState: nil,
32
+ hangDurationMs: nil,
33
+ source: .engine(reportID: reportID),
34
+ digest: Data(SHA256.hash(data: data)),
35
+ ownerNonce: ownerNonce,
36
+ )
37
+ }
38
+
39
+ func digest(_ rawReport: [String: Any]) throws -> Data {
40
+ try Data(SHA256.hash(data: canonicalData(rawReport)))
41
+ }
42
+
43
+ private func canonicalData(_ rawReport: [String: Any]) throws -> Data {
44
+ guard JSONSerialization.isValidJSONObject(rawReport) else {
45
+ throw NativeCrashError(.reportCorrupt)
46
+ }
47
+ do {
48
+ let data = try JSONSerialization.data(withJSONObject: rawReport, options: [.sortedKeys])
49
+ guard data.count <= maxReplayBytes else {
50
+ throw NativeCrashError(.reportCorrupt)
51
+ }
52
+ return data
53
+ } catch let error as NativeCrashError {
54
+ throw error
55
+ } catch {
56
+ throw NativeCrashError(.reportCorrupt)
57
+ }
58
+ }
59
+
60
+ private func mechanism(_ rawValue: String?) -> NativeCrashMechanism {
61
+ switch rawValue {
62
+ case "signal": .signal
63
+ case "mach": .machException
64
+ case "cpp_exception": .cppException
65
+ case "nsexception": .objectiveCException
66
+ case "memory_termination": .memoryTermination
67
+ case "deadlock": .deadlock
68
+ default: .unknown
69
+ }
70
+ }
71
+ }
72
+
73
+ struct NativeCrashTimestamp: Comparable {
74
+ let normalized: String
75
+ private let wholeSecond: String
76
+ private let fractionalNanoseconds: String
77
+
78
+ init?(_ raw: String) {
79
+ guard raw.utf8.count <= 64,
80
+ !raw.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains),
81
+ let components = Self.components(in: raw)
82
+ else {
83
+ return nil
84
+ }
85
+
86
+ let formatter = ISO8601DateFormatter()
87
+ formatter.formatOptions = [.withInternetDateTime]
88
+ guard let date = formatter.date(from: components.wholeSecond) else {
89
+ return nil
90
+ }
91
+ wholeSecond = formatter.string(from: date)
92
+ fractionalNanoseconds = components.fractional
93
+ .padding(toLength: 9, withPad: "0", startingAt: 0)
94
+ if components.fractional.isEmpty {
95
+ normalized = wholeSecond
96
+ } else {
97
+ normalized = "\(wholeSecond.dropLast()).\(components.fractional)Z"
98
+ }
99
+ }
100
+
101
+ static func < (lhs: NativeCrashTimestamp, rhs: NativeCrashTimestamp) -> Bool {
102
+ if lhs.wholeSecond != rhs.wholeSecond {
103
+ return lhs.wholeSecond < rhs.wholeSecond
104
+ }
105
+ return lhs.fractionalNanoseconds < rhs.fractionalNanoseconds
106
+ }
107
+
108
+ static func == (lhs: NativeCrashTimestamp, rhs: NativeCrashTimestamp) -> Bool {
109
+ lhs.wholeSecond == rhs.wholeSecond
110
+ && lhs.fractionalNanoseconds == rhs.fractionalNanoseconds
111
+ }
112
+
113
+ private static func components(in timestamp: String) -> (wholeSecond: String, fractional: String)? {
114
+ guard let timeStart = timestamp.firstIndex(of: "T") else {
115
+ return nil
116
+ }
117
+ guard let separator = timestamp[timeStart...].firstIndex(of: ".") else {
118
+ return (timestamp, "")
119
+ }
120
+ let fractionalStart = timestamp.index(after: separator)
121
+ let suffix = timestamp[fractionalStart...]
122
+ let digits = suffix.prefix {
123
+ $0 >= "0" && $0 <= "9"
124
+ }
125
+ guard (1 ... 9).contains(digits.count),
126
+ let timezoneStart = timestamp.index(
127
+ fractionalStart,
128
+ offsetBy: digits.count,
129
+ limitedBy: timestamp.endIndex,
130
+ )
131
+ else {
132
+ return nil
133
+ }
134
+ return (
135
+ String(timestamp[..<separator] + timestamp[timezoneStart...]),
136
+ String(digits),
137
+ )
138
+ }
139
+ }
@@ -0,0 +1,124 @@
1
+ // Generated by scripts/sync-apple-native-sources.mjs.
2
+ // Edit the canonical Swift source, then regenerate this package boundary.
3
+ import Darwin
4
+ import Foundation
5
+
6
+ enum CrashStorageDirectory {
7
+ static func normalized(_ url: URL) -> URL {
8
+ let parent = url.deletingLastPathComponent().resolvingSymlinksInPath().standardizedFileURL
9
+ return parent.appendingPathComponent(url.lastPathComponent, isDirectory: true).standardizedFileURL
10
+ }
11
+
12
+ static func prepare(_ url: URL) throws -> CrashStorageLease {
13
+ let path = url.path
14
+ try ensurePrivateDirectory(at: url, path: path)
15
+ try protectDirectory(at: url, path: path)
16
+ return try openLease(path: path)
17
+ }
18
+
19
+ private static func ensurePrivateDirectory(at url: URL, path: String) throws {
20
+ var info = stat()
21
+ if lstat(path, &info) == 0 {
22
+ guard info.st_mode & S_IFMT == S_IFDIR,
23
+ info.st_mode & S_IFMT != S_IFLNK
24
+ else {
25
+ throw NativeCrashError(.storageUnsupported)
26
+ }
27
+ } else if errno == ENOENT {
28
+ do {
29
+ try FileManager.default.createDirectory(
30
+ at: url,
31
+ withIntermediateDirectories: false,
32
+ attributes: [.posixPermissions: NSNumber(value: 0o700)],
33
+ )
34
+ } catch {
35
+ throw NativeCrashError(.storageUnsupported)
36
+ }
37
+ } else {
38
+ throw NativeCrashError(.storageUnsupported)
39
+ }
40
+ }
41
+
42
+ private static func protectDirectory(at url: URL, path: String) throws {
43
+ do {
44
+ var attributes: [FileAttributeKey: Any] = [.posixPermissions: NSNumber(value: 0o700)]
45
+ #if os(iOS) || os(tvOS) || os(watchOS)
46
+ attributes[.protectionKey] = FileProtectionType.completeUntilFirstUserAuthentication
47
+ #endif
48
+ try FileManager.default.setAttributes(attributes, ofItemAtPath: path)
49
+ var values = URLResourceValues()
50
+ values.isExcludedFromBackup = true
51
+ var mutableURL = url
52
+ try mutableURL.setResourceValues(values)
53
+ } catch {
54
+ throw NativeCrashError(.storageUnsupported)
55
+ }
56
+ }
57
+
58
+ private static func openLease(path: String) throws -> CrashStorageLease {
59
+ let descriptor = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC)
60
+ guard descriptor >= 0 else {
61
+ throw NativeCrashError(.storageUnsupported)
62
+ }
63
+ var opened = stat()
64
+ var info = stat()
65
+ guard fchmod(descriptor, S_IRWXU) == 0,
66
+ fstat(descriptor, &opened) == 0,
67
+ lstat(path, &info) == 0,
68
+ opened.st_mode & S_IFMT == S_IFDIR,
69
+ opened.st_mode & 0o077 == 0,
70
+ opened.st_dev == info.st_dev,
71
+ opened.st_ino == info.st_ino
72
+ else {
73
+ close(descriptor)
74
+ throw NativeCrashError(.storageUnsupported)
75
+ }
76
+ return CrashStorageLease(
77
+ descriptor: descriptor,
78
+ path: path,
79
+ device: opened.st_dev,
80
+ inode: opened.st_ino,
81
+ )
82
+ }
83
+ }
84
+
85
+ final class CrashStorageLease: @unchecked Sendable {
86
+ private let descriptor: Int32
87
+ private let path: String
88
+ private let device: dev_t
89
+ private let inode: ino_t
90
+
91
+ init(descriptor: Int32, path: String, device: dev_t, inode: ino_t) {
92
+ self.descriptor = descriptor
93
+ self.path = path
94
+ self.device = device
95
+ self.inode = inode
96
+ }
97
+
98
+ deinit {
99
+ close(descriptor)
100
+ }
101
+
102
+ func verify() throws {
103
+ var opened = stat()
104
+ var current = stat()
105
+ guard fstat(descriptor, &opened) == 0,
106
+ lstat(path, &current) == 0,
107
+ opened.st_mode & S_IFMT == S_IFDIR,
108
+ opened.st_mode & 0o077 == 0,
109
+ opened.st_dev == device,
110
+ opened.st_ino == inode,
111
+ current.st_dev == device,
112
+ current.st_ino == inode
113
+ else {
114
+ throw NativeCrashError(.storageUnsupported)
115
+ }
116
+ }
117
+
118
+ func synchronize() throws {
119
+ try verify()
120
+ guard fsync(descriptor) == 0 else {
121
+ throw NativeCrashError(.storageUnsupported)
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,137 @@
1
+ // Generated by scripts/sync-apple-native-sources.mjs.
2
+ // Edit the canonical Swift source, then regenerate this package boundary.
3
+ import Foundation
4
+
5
+ @objc(LBRNAppleNativeArtifactIdentity)
6
+ @objcMembers
7
+ public final class NativeArtifactIdentity: NSObject, @unchecked Sendable {
8
+ public let projectId: String
9
+ @nonobjc public let release: String
10
+ public let environment: String
11
+ public let service: String
12
+
13
+ public init(
14
+ projectId: String,
15
+ release: String,
16
+ environment: String,
17
+ service: String,
18
+ ) throws {
19
+ guard UUID(uuidString: projectId)?.uuidString.lowercased() == projectId,
20
+ Self.isExactContext(release),
21
+ Self.isExactContext(environment),
22
+ Self.isExactContext(service)
23
+ else {
24
+ throw NativeCrashError(.invalidConfiguration)
25
+ }
26
+ self.projectId = projectId
27
+ self.release = release
28
+ self.environment = environment
29
+ self.service = service
30
+ super.init()
31
+ }
32
+
33
+ override public func isEqual(_ object: Any?) -> Bool {
34
+ guard let other = object as? NativeArtifactIdentity else {
35
+ return false
36
+ }
37
+ return projectId == other.projectId
38
+ && release == other.release
39
+ && environment == other.environment
40
+ && service == other.service
41
+ }
42
+
43
+ override public var hash: Int {
44
+ var hasher = Hasher()
45
+ hasher.combine(projectId)
46
+ hasher.combine(release)
47
+ hasher.combine(environment)
48
+ hasher.combine(service)
49
+ return hasher.finalize()
50
+ }
51
+
52
+ override public var description: String {
53
+ "NativeArtifactIdentity(configured: true)"
54
+ }
55
+
56
+ private static func isExactContext(_ value: String) -> Bool {
57
+ guard (1 ... 256).contains(value.utf8.count),
58
+ value == value.trimmingCharacters(in: .whitespacesAndNewlines),
59
+ !value.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains)
60
+ else {
61
+ return false
62
+ }
63
+ return true
64
+ }
65
+ }
66
+
67
+ struct NativeArtifactIdentityValue: Codable, Equatable {
68
+ private static let reportKey = "logbrew_native_artifact_identity"
69
+ private static let reportFields = [
70
+ "environment",
71
+ "project_id",
72
+ "release",
73
+ "service",
74
+ "version",
75
+ ]
76
+
77
+ let projectId: String
78
+ let release: String
79
+ let environment: String
80
+ let service: String
81
+
82
+ init(_ identity: NativeArtifactIdentity) {
83
+ projectId = identity.projectId
84
+ release = identity.release
85
+ environment = identity.environment
86
+ service = identity.service
87
+ }
88
+
89
+ func validatedIdentity() throws -> NativeArtifactIdentity {
90
+ try NativeArtifactIdentity(
91
+ projectId: projectId,
92
+ release: release,
93
+ environment: environment,
94
+ service: service,
95
+ )
96
+ }
97
+
98
+ var crashUserInfoJSON: [String: Any] {
99
+ [
100
+ Self.reportKey: [
101
+ "version": 1,
102
+ "project_id": projectId,
103
+ "release": release,
104
+ "environment": environment,
105
+ "service": service,
106
+ ],
107
+ ]
108
+ }
109
+
110
+ static func persistedIdentity(in rawReport: [String: Any]) throws -> NativeArtifactIdentity? {
111
+ guard let user = rawReport["user"] as? [String: Any],
112
+ let persisted = user[reportKey]
113
+ else {
114
+ return nil
115
+ }
116
+ guard let values = persisted as? [String: Any],
117
+ values.keys.sorted() == reportFields,
118
+ values["version"] as? Int == 1,
119
+ let projectId = values["project_id"] as? String,
120
+ let release = values["release"] as? String,
121
+ let environment = values["environment"] as? String,
122
+ let service = values["service"] as? String
123
+ else {
124
+ throw NativeCrashError(.reportCorrupt)
125
+ }
126
+ do {
127
+ return try NativeArtifactIdentity(
128
+ projectId: projectId,
129
+ release: release,
130
+ environment: environment,
131
+ service: service,
132
+ )
133
+ } catch {
134
+ throw NativeCrashError(.reportCorrupt)
135
+ }
136
+ }
137
+ }