@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.
- package/LogBrewReactNative.podspec +25 -3
- package/README.md +106 -3
- package/apple-native-diagnostics.d.ts +52 -0
- package/apple-native-diagnostics.js +286 -0
- package/expo.cjs +46 -0
- package/expo.d.ts +16 -0
- package/expo.js +5 -0
- package/fatal-replay.cjs +12 -1
- package/global-errors.cjs +3 -1
- package/index.cjs +84 -2
- package/index.d.cts +4 -0
- package/index.d.ts +4 -0
- package/index.js +84 -2
- package/index.native.d.ts +4 -0
- package/index.native.js +14 -1
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
- package/package.json +42 -4
- package/promise-rejections.cjs +3 -1
- package/scripts/sync-apple-native-sources.mjs +93 -0
- 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
|
+
}
|