@logbrew/react-native 0.1.11 → 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 (61) hide show
  1. package/LogBrewReactNative.podspec +25 -3
  2. package/README.md +106 -3
  3. package/apple-native-diagnostics.d.ts +52 -0
  4. package/apple-native-diagnostics.js +286 -0
  5. package/expo.cjs +46 -0
  6. package/expo.d.ts +16 -0
  7. package/expo.js +5 -0
  8. package/fatal-replay.cjs +12 -1
  9. package/global-errors.cjs +3 -1
  10. package/index.cjs +84 -2
  11. package/index.d.cts +4 -0
  12. package/index.d.ts +4 -0
  13. package/index.js +84 -2
  14. package/index.native.d.ts +4 -0
  15. package/index.native.js +14 -1
  16. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
  17. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
  18. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
  19. package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
  20. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
  21. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
  22. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
  23. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
  29. package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
  30. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
  31. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
  32. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
  33. package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
  34. package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
  35. package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
  36. package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
  37. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
  38. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
  39. package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
  40. package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
  41. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
  42. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
  43. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
  44. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
  45. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
  46. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
  47. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
  48. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
  49. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
  50. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
  51. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
  52. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
  53. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
  54. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
  55. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
  56. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
  57. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
  58. package/package.json +42 -4
  59. package/promise-rejections.cjs +3 -1
  60. package/scripts/sync-apple-native-sources.mjs +93 -0
  61. package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
@@ -0,0 +1,229 @@
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
+ @objc(LBRNAppleNativeCrashCapture)
7
+ @objcMembers
8
+ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
9
+ let configuration: NativeCrashConfiguration
10
+ let driver: any CrashEngineDriving
11
+ let ownership: ProcessCrashCaptureOwnership
12
+ let processIDProvider: @Sendable () -> Int32
13
+ let ownerProcessID: Int32
14
+ let ownerNonce = UUID()
15
+ let lock = NSLock()
16
+
17
+ var store: (any CrashReportStoring)?
18
+ var hangStore: (any HangIncidentStoring)?
19
+ var hangRuntime: NativeHangWatchdogRuntime?
20
+ var storageLease: CrashStorageLease?
21
+ var lifecycle: NativeCrashLifecycleState = .idle
22
+ var lastOutcome: NativeCrashOutcome = .none
23
+ var acknowledged = 0
24
+ var discarded = 0
25
+ var replaying = false
26
+
27
+ public init(configuration: NativeCrashConfiguration) {
28
+ let processIDProvider: @Sendable () -> Int32 = { getpid() }
29
+ self.configuration = configuration
30
+ driver = KSCrashEngineDriver()
31
+ ownership = .shared
32
+ self.processIDProvider = processIDProvider
33
+ ownerProcessID = processIDProvider()
34
+ super.init()
35
+ }
36
+
37
+ init(
38
+ configuration: NativeCrashConfiguration,
39
+ driver: any CrashEngineDriving,
40
+ ownership: ProcessCrashCaptureOwnership,
41
+ processIDProvider: @escaping @Sendable () -> Int32 = { getpid() },
42
+ ) {
43
+ self.configuration = configuration
44
+ self.driver = driver
45
+ self.ownership = ownership
46
+ self.processIDProvider = processIDProvider
47
+ ownerProcessID = processIDProvider()
48
+ super.init()
49
+ }
50
+
51
+ public func install() throws {
52
+ lock.lock()
53
+ defer { lock.unlock() }
54
+ try verifyProcessLocked()
55
+
56
+ if store != nil, lifecycle != .stopped {
57
+ try verifyStorageLocked()
58
+ return
59
+ }
60
+ if lifecycle == .stopped {
61
+ throw NativeCrashError(.notInstalled)
62
+ }
63
+ if lifecycle == .failed {
64
+ throw NativeCrashError(.engineInstallFailed)
65
+ }
66
+
67
+ do {
68
+ let storageLease = try CrashStorageDirectory.prepare(configuration.storageDirectory)
69
+ let preparedHang = try prepareHangWatchdog()
70
+ try ownership.claim(self)
71
+ try storageLease.verify()
72
+ self.storageLease = storageLease
73
+ let installedStore = try driver.install(
74
+ configuration: CrashEngineConfiguration(
75
+ storageDirectory: configuration.storageDirectory,
76
+ maxStoredReports: configuration.maxStoredReports,
77
+ monitors: [.machException, .signal, .cppException, .objectiveCException],
78
+ includesMemory: false,
79
+ includesQueueNames: false,
80
+ includesConsoleLog: false,
81
+ artifactIdentity: configuration.artifactIdentity.map(NativeArtifactIdentityValue.init),
82
+ deletionIsExplicit: true,
83
+ ),
84
+ )
85
+ try storageLease.verify()
86
+ store = installedStore
87
+ hangStore = preparedHang?.store
88
+ hangRuntime = preparedHang?.runtime
89
+ preparedHang?.runtime.start()
90
+ lifecycle = .installed
91
+ } catch let error as NativeCrashError {
92
+ lifecycle = .failed
93
+ lastOutcome = .failed
94
+ throw error
95
+ } catch {
96
+ lifecycle = .failed
97
+ lastOutcome = .failed
98
+ throw NativeCrashError(.engineInstallFailed)
99
+ }
100
+ }
101
+
102
+ public func pendingReports() throws -> [NativeCrashRecord] {
103
+ lock.lock()
104
+ defer { lock.unlock() }
105
+ try verifyProcessLocked()
106
+ return try pendingReportsLocked()
107
+ }
108
+
109
+ @objc(replayPendingReportsWithHandler:error:)
110
+ public func replayPendingReports(
111
+ _ handler: (NativeCrashRecord) -> Bool,
112
+ ) throws -> NativeCrashReplayResult {
113
+ try beginReplay()
114
+ do {
115
+ return try performReplay(handler)
116
+ } catch {
117
+ failReplay()
118
+ throw error
119
+ }
120
+ }
121
+
122
+ /// Replays stored reports through the existing delivery engine and acknowledges only accepted reports.
123
+ @nonobjc
124
+ public func replayPendingReports(
125
+ in client: LogBrewClient,
126
+ transport: any Transport,
127
+ ) throws -> NativeCrashReplayResult {
128
+ try replayPendingReports { record in
129
+ do {
130
+ try record.enqueue(in: client)
131
+ return try client.flushEvent(
132
+ record.eventID,
133
+ transport: transport,
134
+ )
135
+ } catch {
136
+ return false
137
+ }
138
+ }
139
+ }
140
+
141
+ /// Stops replay through this adapter without claiming to uninstall the process-lifetime crash handler.
142
+ public func stopReplay() throws {
143
+ lock.lock()
144
+ defer { lock.unlock() }
145
+ try verifyProcessLocked()
146
+ guard store != nil else {
147
+ throw NativeCrashError(.notInstalled)
148
+ }
149
+ guard !replaying else {
150
+ throw NativeCrashError(.replayBusy)
151
+ }
152
+ hangRuntime?.stop()
153
+ lifecycle = .stopped
154
+ }
155
+
156
+ public func purge() throws {
157
+ lock.lock()
158
+ defer { lock.unlock() }
159
+ try verifyProcessLocked()
160
+ guard let store else {
161
+ throw NativeCrashError(.notInstalled)
162
+ }
163
+ try verifyStorageLocked()
164
+ guard !replaying else {
165
+ throw NativeCrashError(.replayBusy)
166
+ }
167
+
168
+ store.deleteAllReports()
169
+ guard store.reportIDs.isEmpty else {
170
+ lastOutcome = .failed
171
+ throw NativeCrashError(.reportDeletionFailed)
172
+ }
173
+ try hangStore?.purge()
174
+ lastOutcome = .purged
175
+ }
176
+
177
+ public func status() throws -> NativeCrashStatus {
178
+ lock.lock()
179
+ defer { lock.unlock() }
180
+ try verifyProcessLocked()
181
+ let pending = store == nil ? 0 : try pendingCountLocked()
182
+ return NativeCrashStatus(
183
+ lifecycle: lifecycle,
184
+ pending: pending,
185
+ acknowledged: acknowledged,
186
+ discarded: discarded,
187
+ lastOutcome: lastOutcome,
188
+ )
189
+ }
190
+ }
191
+
192
+ extension NativeCrashCapture {
193
+ func verifyStorageLocked() throws {
194
+ guard let storageLease else {
195
+ throw NativeCrashError(.notInstalled)
196
+ }
197
+ try storageLease.verify()
198
+ }
199
+
200
+ func verifyProcessLocked() throws {
201
+ guard processIDProvider() == ownerProcessID else {
202
+ throw NativeCrashError(.processChanged)
203
+ }
204
+ }
205
+ }
206
+
207
+ private extension NativeCrashCapture {
208
+ func prepareHangWatchdog() throws -> PreparedHangWatchdog? {
209
+ guard let watchdog = configuration.hangWatchdog,
210
+ let identity = configuration.artifactIdentity
211
+ else {
212
+ return nil
213
+ }
214
+ let directory = configuration.storageDirectory
215
+ .appendingPathComponent("hang-watchdog-v1", isDirectory: true)
216
+ let store = try NativeHangIncidentFileStore(directory: directory)
217
+ let runtime = try NativeHangWatchdogRuntime(
218
+ configuration: watchdog,
219
+ identity: identity,
220
+ store: store,
221
+ )
222
+ return PreparedHangWatchdog(store: store, runtime: runtime)
223
+ }
224
+ }
225
+
226
+ private struct PreparedHangWatchdog {
227
+ let store: any HangIncidentStoring
228
+ let runtime: NativeHangWatchdogRuntime
229
+ }
@@ -0,0 +1,386 @@
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(LBRNAppleNativeCrashMechanism)
6
+ public enum NativeCrashMechanism: Int, Sendable {
7
+ case signal
8
+ case machException
9
+ case cppException
10
+ case objectiveCException
11
+ case memoryTermination
12
+ case deadlock
13
+ case unknown
14
+
15
+ public var name: String {
16
+ switch self {
17
+ case .signal:
18
+ "signal"
19
+ case .machException:
20
+ "mach"
21
+ case .cppException:
22
+ "cpp_exception"
23
+ case .objectiveCException:
24
+ "nsexception"
25
+ case .memoryTermination:
26
+ "memory_termination"
27
+ case .deadlock:
28
+ "deadlock"
29
+ case .unknown:
30
+ "unknown"
31
+ }
32
+ }
33
+ }
34
+
35
+ @objc(LBRNAppleNativeCrashLifecycleState)
36
+ public enum NativeCrashLifecycleState: Int, Sendable {
37
+ case idle
38
+ case installed
39
+ case replaying
40
+ case failed
41
+ case stopped
42
+ }
43
+
44
+ @objc(LBRNAppleNativeCrashOutcome)
45
+ public enum NativeCrashOutcome: Int, Sendable {
46
+ case none
47
+ case acknowledged
48
+ case retained
49
+ case purged
50
+ case failed
51
+ case discarded
52
+ }
53
+
54
+ public enum NativeCrashErrorCode: String, Sendable {
55
+ case invalidConfiguration = "crash_invalid_configuration"
56
+ case storageUnsupported = "crash_storage_unsupported"
57
+ case ownershipConflict = "crash_capture_owned"
58
+ case notInstalled = "crash_capture_not_installed"
59
+ case engineInstallFailed = "crash_engine_install_failed"
60
+ case replayBusy = "crash_replay_busy"
61
+ case reportCorrupt = "crash_report_corrupt"
62
+ case reportChanged = "crash_report_changed"
63
+ case reportDeletionFailed = "crash_report_delete_failed"
64
+ case processChanged = "crash_process_changed"
65
+ }
66
+
67
+ public struct NativeCrashError: Error, CustomNSError, LocalizedError, Sendable {
68
+ public let code: NativeCrashErrorCode
69
+
70
+ init(_ code: NativeCrashErrorCode) {
71
+ self.code = code
72
+ }
73
+
74
+ public static var errorDomain: String {
75
+ "co.logbrew.native-crash"
76
+ }
77
+
78
+ public var errorCode: Int {
79
+ switch code {
80
+ case .invalidConfiguration: 1
81
+ case .storageUnsupported: 2
82
+ case .ownershipConflict: 3
83
+ case .notInstalled: 4
84
+ case .engineInstallFailed: 5
85
+ case .replayBusy: 6
86
+ case .reportCorrupt: 7
87
+ case .reportChanged: 8
88
+ case .reportDeletionFailed: 9
89
+ case .processChanged: 10
90
+ }
91
+ }
92
+
93
+ public var errorDescription: String? {
94
+ code.rawValue
95
+ }
96
+
97
+ public var errorUserInfo: [String: Any] {
98
+ [NSLocalizedDescriptionKey: code.rawValue]
99
+ }
100
+ }
101
+
102
+ @objc(LBRNAppleNativeCrashConfiguration)
103
+ @objcMembers
104
+ public final class NativeCrashConfiguration: NSObject, @unchecked Sendable {
105
+ public let storageDirectory: URL
106
+ public let maxStoredReports: Int
107
+ public let maxReplayBytes: Int
108
+ public let artifactIdentity: NativeArtifactIdentity?
109
+ @nonobjc public let hangWatchdog: NativeHangWatchdogConfiguration?
110
+
111
+ public convenience init(
112
+ storageDirectory: URL,
113
+ maxStoredReports: Int = 5,
114
+ maxReplayBytes: Int = 4 * 1024 * 1024,
115
+ ) throws {
116
+ try self.init(
117
+ storageDirectory: storageDirectory,
118
+ maxStoredReports: maxStoredReports,
119
+ maxReplayBytes: maxReplayBytes,
120
+ artifactIdentity: nil,
121
+ hangWatchdog: nil,
122
+ )
123
+ }
124
+
125
+ @nonobjc
126
+ public init(
127
+ storageDirectory: URL,
128
+ maxStoredReports: Int = 5,
129
+ maxReplayBytes: Int = 4 * 1024 * 1024,
130
+ artifactIdentity: NativeArtifactIdentity?,
131
+ hangWatchdog: NativeHangWatchdogConfiguration?,
132
+ ) throws {
133
+ guard storageDirectory.isFileURL,
134
+ !storageDirectory.path.isEmpty,
135
+ storageDirectory.path != "/",
136
+ (1 ... 32).contains(maxStoredReports),
137
+ (1024 ... 16 * 1024 * 1024).contains(maxReplayBytes),
138
+ hangWatchdog == nil || artifactIdentity != nil
139
+ else {
140
+ throw NativeCrashError(.invalidConfiguration)
141
+ }
142
+
143
+ let normalizedDirectory = CrashStorageDirectory.normalized(storageDirectory)
144
+ guard normalizedDirectory.path != "/",
145
+ !["", ".", ".."].contains(storageDirectory.lastPathComponent)
146
+ else {
147
+ throw NativeCrashError(.invalidConfiguration)
148
+ }
149
+ self.storageDirectory = normalizedDirectory
150
+ self.maxStoredReports = maxStoredReports
151
+ self.maxReplayBytes = maxReplayBytes
152
+ self.artifactIdentity = artifactIdentity
153
+ self.hangWatchdog = hangWatchdog
154
+ }
155
+ }
156
+
157
+ @objc(LBRNAppleNativeCrashRecord)
158
+ @objcMembers
159
+ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
160
+ public let eventID: String
161
+ public let timestamp: String
162
+ public let mechanism: NativeCrashMechanism
163
+
164
+ private let nativeStackFrames: [NativeStackFrame]?
165
+ private let artifactIdentity: NativeArtifactIdentity?
166
+ private let hangState: NativeHangIncidentState?
167
+ private let hangDurationMs: Double?
168
+ let source: NativeCrashRecordSource
169
+ let digest: Data
170
+ let ownerNonce: UUID
171
+
172
+ init(
173
+ eventID: String,
174
+ timestamp: String,
175
+ mechanism: NativeCrashMechanism,
176
+ nativeStackFrames: [NativeStackFrame]?,
177
+ artifactIdentity: NativeArtifactIdentity?,
178
+ hangState: NativeHangIncidentState?,
179
+ hangDurationMs: Double? = nil,
180
+ source: NativeCrashRecordSource,
181
+ digest: Data,
182
+ ownerNonce: UUID,
183
+ ) {
184
+ self.eventID = eventID
185
+ self.timestamp = timestamp
186
+ self.mechanism = mechanism
187
+ self.nativeStackFrames = nativeStackFrames
188
+ self.artifactIdentity = artifactIdentity
189
+ self.hangState = hangState
190
+ self.hangDurationMs = hangDurationMs
191
+ self.source = source
192
+ self.digest = digest
193
+ self.ownerNonce = ownerNonce
194
+ }
195
+
196
+ @nonobjc
197
+ public func enqueue(in client: LogBrewClient) throws {
198
+ let existing = try existingEvent(in: client)
199
+ if existing == .matching {
200
+ return
201
+ }
202
+ if existing == .collision {
203
+ throw NativeCrashError(.reportChanged)
204
+ }
205
+
206
+ try client.issueDetached(
207
+ eventID,
208
+ timestamp: timestamp,
209
+ attributes: issueAttributes,
210
+ )
211
+ }
212
+
213
+ override public var description: String {
214
+ "NativeCrashRecord(mechanism: \(mechanism.name), configuredIdentity: \(artifactIdentity != nil))"
215
+ }
216
+
217
+ private var issueAttributes: IssueAttributes {
218
+ var metadata: Metadata = [
219
+ "crash.mechanism": .string(mechanism.name),
220
+ "crash.replayed": .bool(true),
221
+ ]
222
+ if let artifactIdentity {
223
+ metadata["projectId"] = .string(artifactIdentity.projectId)
224
+ metadata["release"] = .string(artifactIdentity.release)
225
+ metadata["environment"] = .string(artifactIdentity.environment)
226
+ metadata["service"] = .string(artifactIdentity.service)
227
+ }
228
+ if let hangState {
229
+ metadata["crash.handled"] = .bool(hangState == .recovered)
230
+ }
231
+ if let hangDurationMs {
232
+ metadata["durationMs"] = .double(hangDurationMs)
233
+ }
234
+ return IssueAttributes(
235
+ title: hangState == nil ? "Native application crash" : "Native application hang",
236
+ level: hangState == .recovered ? .error : .fatal,
237
+ exception: IssueException(
238
+ type: hangState == nil ? "AppleNativeCrash" : "AppleNativeHang",
239
+ mechanism: IssueExceptionMechanism(
240
+ type: mechanism.name,
241
+ handled: hangState == .recovered,
242
+ ),
243
+ ),
244
+ metadata: metadata,
245
+ nativeStackFrames: nativeStackFrames,
246
+ )
247
+ }
248
+
249
+ private func existingEvent(in client: LogBrewClient) throws -> ExistingEvent {
250
+ guard let data = try client.previewJSON().data(using: .utf8),
251
+ let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any],
252
+ let events = payload["events"] as? [[String: Any]]
253
+ else {
254
+ throw NativeCrashError(.reportChanged)
255
+ }
256
+ guard let event = events.first(where: { $0["id"] as? String == eventID }) else {
257
+ return .absent
258
+ }
259
+ guard event["type"] as? String == "issue",
260
+ event["timestamp"] as? String == timestamp,
261
+ let attributes = event["attributes"] as? [String: Any],
262
+ attributes["title"] as? String == issueAttributes.title,
263
+ attributes["level"] as? String == issueAttributes.level.canonicalValue,
264
+ attributes["message"] == nil,
265
+ exceptionMatches(attributes["exception"]),
266
+ attributes["stackFrames"] == nil,
267
+ attributes["breadcrumbs"] == nil,
268
+ attributes["breadcrumbsTruncated"] == nil,
269
+ attributes["context"] == nil,
270
+ let metadata = attributes["metadata"] as? [String: Any],
271
+ metadata as NSDictionary == expectedMetadata,
272
+ nativeStackFramesMatch(attributes["nativeStackFrames"])
273
+ else {
274
+ return .collision
275
+ }
276
+ return .matching
277
+ }
278
+
279
+ private var expectedMetadata: NSDictionary {
280
+ var metadata: [String: Any] = [
281
+ "crash.mechanism": mechanism.name,
282
+ "crash.replayed": true,
283
+ ]
284
+ if let artifactIdentity {
285
+ metadata["projectId"] = artifactIdentity.projectId
286
+ metadata["release"] = artifactIdentity.release
287
+ metadata["environment"] = artifactIdentity.environment
288
+ metadata["service"] = artifactIdentity.service
289
+ }
290
+ if let hangState {
291
+ metadata["crash.handled"] = hangState == .recovered
292
+ }
293
+ if let hangDurationMs {
294
+ metadata["durationMs"] = hangDurationMs
295
+ }
296
+ return metadata as NSDictionary
297
+ }
298
+
299
+ private var expectedException: NSDictionary {
300
+ [
301
+ "type": hangState == nil ? "AppleNativeCrash" : "AppleNativeHang",
302
+ "mechanism": [
303
+ "type": mechanism.name,
304
+ "handled": hangState == .recovered,
305
+ ],
306
+ ] as NSDictionary
307
+ }
308
+
309
+ private func exceptionMatches(_ value: Any?) -> Bool {
310
+ guard let value else {
311
+ // Preserve exact retry identity for an event queued by an older SDK before typed exceptions existed.
312
+ return true
313
+ }
314
+ guard let exception = value as? [String: Any] else {
315
+ return false
316
+ }
317
+ return exception as NSDictionary == expectedException
318
+ }
319
+
320
+ private func nativeStackFramesMatch(_ value: Any?) -> Bool {
321
+ guard let nativeStackFrames else {
322
+ return value == nil
323
+ }
324
+ guard let frames = value as? [[String: Any]], frames.count == nativeStackFrames.count else {
325
+ return false
326
+ }
327
+ return zip(frames, nativeStackFrames).allSatisfy { frame, expected in
328
+ frame as NSDictionary == [
329
+ "imageUuid": expected.imageUuid,
330
+ "architecture": expected.architecture.rawValue,
331
+ "instructionOffset": expected.instructionOffset,
332
+ ] as NSDictionary
333
+ }
334
+ }
335
+ }
336
+
337
+ enum NativeCrashRecordSource: Equatable {
338
+ case engine(reportID: Int64)
339
+ case hang(eventID: String)
340
+ }
341
+
342
+ private enum ExistingEvent {
343
+ case absent
344
+ case matching
345
+ case collision
346
+ }
347
+
348
+ @objc(LBRNAppleNativeCrashReplayResult)
349
+ @objcMembers
350
+ public final class NativeCrashReplayResult: NSObject, @unchecked Sendable {
351
+ public let attempted: Int
352
+ public let acknowledged: Int
353
+ public let discarded: Int
354
+ public let pending: Int
355
+
356
+ init(attempted: Int, acknowledged: Int, discarded: Int, pending: Int) {
357
+ self.attempted = attempted
358
+ self.acknowledged = acknowledged
359
+ self.discarded = discarded
360
+ self.pending = pending
361
+ }
362
+ }
363
+
364
+ @objc(LBRNAppleNativeCrashStatus)
365
+ @objcMembers
366
+ public final class NativeCrashStatus: NSObject, @unchecked Sendable {
367
+ public let lifecycle: NativeCrashLifecycleState
368
+ public let pending: Int
369
+ public let acknowledged: Int
370
+ public let discarded: Int
371
+ public let lastOutcome: NativeCrashOutcome
372
+
373
+ init(
374
+ lifecycle: NativeCrashLifecycleState,
375
+ pending: Int,
376
+ acknowledged: Int,
377
+ discarded: Int,
378
+ lastOutcome: NativeCrashOutcome,
379
+ ) {
380
+ self.lifecycle = lifecycle
381
+ self.pending = pending
382
+ self.acknowledged = acknowledged
383
+ self.discarded = discarded
384
+ self.lastOutcome = lastOutcome
385
+ }
386
+ }