@logbrew/react-native 0.1.19 → 0.1.21
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/README.md +24 -1
- package/apple-native-diagnostics.d.ts +20 -0
- package/apple-native-diagnostics.js +111 -1
- package/index.cjs +1 -1
- package/index.js +36 -1207
- package/index.native.d.ts +1 -0
- package/index.native.js +62 -6
- package/instrumentation.js +3 -632
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +10 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +48 -1
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +3 -2
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +16 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +5 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +26 -10
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +35 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCorrelation.swift +174 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +30 -1
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +7 -6
- package/metadata.js +10 -181
- package/package.json +2 -2
- package/src/NativeLogBrewAppleDiagnostics.ts +6 -0
|
@@ -174,7 +174,7 @@ final class IssueBreadcrumbStore: @unchecked Sendable {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
let maximumIssueBreadcrumbs = 64
|
|
177
|
+
@_spi(CrashReplay) public let maximumIssueBreadcrumbs = 64
|
|
178
178
|
|
|
179
179
|
func normalizeIssueAttributes(_ value: IssueAttributes) throws -> IssueAttributes {
|
|
180
180
|
let exception = try value.exception.map(validateIssueException)
|
|
@@ -251,7 +251,8 @@ func validateIssueException(_ value: IssueException) throws -> IssueException {
|
|
|
251
251
|
return IssueException(type: exceptionType, mechanism: mechanism)
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
@_spi(CrashReplay)
|
|
255
|
+
public func validateIssueBreadcrumb(_ value: IssueBreadcrumb) throws -> IssueBreadcrumb {
|
|
255
256
|
try requireTimestamp(value.timestamp)
|
|
256
257
|
guard validMachineKey(value.category, maximum: 64, separators: ["_", ".", ":", "-"]) else {
|
|
257
258
|
throw issueValidationError("issue breadcrumb category is invalid")
|
|
@@ -43,6 +43,22 @@ func validateTelemetryContext(
|
|
|
43
43
|
return TelemetryContext(resource: resource, trace: trace, session: session, subject: subject, tags: tags)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
@_spi(CrashReplay)
|
|
47
|
+
public func validateNativeCrashCorrelationContext(_ value: TelemetryContext) throws -> TelemetryContext {
|
|
48
|
+
let context = try validateTelemetryContext(value, label: "native crash correlation context")
|
|
49
|
+
let identifiers = [context.session?.id, context.session?.previousId, context.subject?.id].compactMap(\.self)
|
|
50
|
+
guard context.resource == nil,
|
|
51
|
+
context.tags == nil,
|
|
52
|
+
context.trace != nil || context.session != nil || context.subject != nil,
|
|
53
|
+
identifiers.allSatisfy({
|
|
54
|
+
validMachineKey($0, maximum: 200, separators: ["_", "-"], allowNumericStart: true)
|
|
55
|
+
})
|
|
56
|
+
else {
|
|
57
|
+
throw contextValidationError("native crash correlation context is invalid")
|
|
58
|
+
}
|
|
59
|
+
return context
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
func telemetryTraceContext(_ context: LogBrewTraceContext) -> TelemetryTraceContext {
|
|
47
63
|
TelemetryTraceContext(
|
|
48
64
|
traceId: context.traceId,
|
|
@@ -23,6 +23,7 @@ struct CrashEngineConfiguration: Equatable {
|
|
|
23
23
|
|
|
24
24
|
protocol CrashEngineDriving: AnyObject {
|
|
25
25
|
func install(configuration: CrashEngineConfiguration) throws -> any CrashReportStoring
|
|
26
|
+
func setUserInfo(_ value: String?, forKey key: String)
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
protocol CrashReportStoring: AnyObject {
|
|
@@ -98,6 +99,10 @@ final class KSCrashEngineDriver: CrashEngineDriving {
|
|
|
98
99
|
}
|
|
99
100
|
return KSCrashReportStoreAdapter(store: reportStore)
|
|
100
101
|
}
|
|
102
|
+
|
|
103
|
+
func setUserInfo(_ value: String?, forKey key: String) {
|
|
104
|
+
KSCrash.shared.setUserInfo(value, forKey: key)
|
|
105
|
+
}
|
|
101
106
|
}
|
|
102
107
|
|
|
103
108
|
private final class KSCrashReportStoreAdapter: CrashReportStoring {
|
|
@@ -22,6 +22,8 @@ struct CrashReportSanitizer {
|
|
|
22
22
|
let error = crash?["error"] as? [String: Any]
|
|
23
23
|
let nativeStackFrames = NativeStackFrameSanitizer().frames(from: rawReport)
|
|
24
24
|
let artifactIdentity = try NativeArtifactIdentityValue.persistedIdentity(in: rawReport)
|
|
25
|
+
let correlation = NativeCrashCorrelation.captured(in: rawReport)
|
|
26
|
+
let breadcrumbs = NativeCrashBreadcrumbs.captured(in: rawReport)
|
|
25
27
|
return NativeCrashRecord(
|
|
26
28
|
eventID: uuid.uuidString.lowercased(),
|
|
27
29
|
timestamp: timestamp.normalized,
|
|
@@ -31,7 +33,12 @@ struct CrashReportSanitizer {
|
|
|
31
33
|
context: nativeCrashContext(
|
|
32
34
|
system: rawReport["system"] as? [String: Any],
|
|
33
35
|
artifactIdentity: artifactIdentity,
|
|
36
|
+
correlation: correlation.0,
|
|
34
37
|
),
|
|
38
|
+
correlationState: correlation.1,
|
|
39
|
+
breadcrumbs: breadcrumbs.0?.breadcrumbs,
|
|
40
|
+
breadcrumbsTruncated: breadcrumbs.0?.truncated,
|
|
41
|
+
breadcrumbState: breadcrumbs.1,
|
|
35
42
|
hangState: nil,
|
|
36
43
|
hangDurationMs: nil,
|
|
37
44
|
source: .engine(reportID: reportID),
|
|
@@ -77,6 +84,7 @@ struct CrashReportSanitizer {
|
|
|
77
84
|
func nativeCrashContext(
|
|
78
85
|
system: [String: Any]?,
|
|
79
86
|
artifactIdentity: NativeArtifactIdentity?,
|
|
87
|
+
correlation: TelemetryContext? = nil,
|
|
80
88
|
) -> TelemetryContext? {
|
|
81
89
|
let value = { (key: String) in crashContextValue(system?[key]) }
|
|
82
90
|
let operatingSystem = value("system_name").map {
|
|
@@ -94,18 +102,26 @@ func nativeCrashContext(
|
|
|
94
102
|
let application = applicationName == nil && applicationVersion == nil && applicationBuild == nil
|
|
95
103
|
? nil
|
|
96
104
|
: TelemetryApplication(name: applicationName, version: applicationVersion, build: applicationBuild)
|
|
97
|
-
|
|
105
|
+
let resource = artifactIdentity != nil || operatingSystem != nil || device != nil || application != nil
|
|
106
|
+
? TelemetryResource(
|
|
107
|
+
service: artifactIdentity.map { TelemetryNamedVersion(name: $0.service) },
|
|
108
|
+
deployment: artifactIdentity.map {
|
|
109
|
+
TelemetryDeployment(environment: $0.environment, release: $0.release)
|
|
110
|
+
},
|
|
111
|
+
operatingSystem: operatingSystem,
|
|
112
|
+
device: device,
|
|
113
|
+
application: application,
|
|
114
|
+
)
|
|
115
|
+
: nil
|
|
116
|
+
guard resource != nil || correlation != nil else {
|
|
98
117
|
return nil
|
|
99
118
|
}
|
|
100
|
-
return TelemetryContext(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
device: device,
|
|
107
|
-
application: application,
|
|
108
|
-
))
|
|
119
|
+
return TelemetryContext(
|
|
120
|
+
resource: resource,
|
|
121
|
+
trace: correlation?.trace,
|
|
122
|
+
session: correlation?.session,
|
|
123
|
+
subject: correlation?.subject,
|
|
124
|
+
)
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
private func crashContextValue(_ rawValue: Any?) -> String? {
|
|
@@ -106,6 +106,41 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
|
|
|
106
106
|
return try pendingReportsLocked()
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/// Replaces the crash-time trace, session, and opaque subject snapshot in one bounded write.
|
|
110
|
+
@nonobjc
|
|
111
|
+
public func setCorrelationContext(_ context: TelemetryContext?) throws {
|
|
112
|
+
lock.lock()
|
|
113
|
+
defer { lock.unlock() }
|
|
114
|
+
try verifyProcessLocked()
|
|
115
|
+
guard store != nil, lifecycle != .stopped else {
|
|
116
|
+
throw NativeCrashError(.notInstalled)
|
|
117
|
+
}
|
|
118
|
+
try verifyStorageLocked()
|
|
119
|
+
try driver.setUserInfo(
|
|
120
|
+
NativeCrashCorrelation.encoded(context),
|
|
121
|
+
forKey: NativeCrashCorrelation.reportKey,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Replaces the crash-time breadcrumb snapshot in one bounded write.
|
|
126
|
+
@nonobjc
|
|
127
|
+
public func setBreadcrumbs(
|
|
128
|
+
_ breadcrumbs: [IssueBreadcrumb]?,
|
|
129
|
+
truncated: Bool = false,
|
|
130
|
+
) throws {
|
|
131
|
+
lock.lock()
|
|
132
|
+
defer { lock.unlock() }
|
|
133
|
+
try verifyProcessLocked()
|
|
134
|
+
guard store != nil, lifecycle != .stopped else {
|
|
135
|
+
throw NativeCrashError(.notInstalled)
|
|
136
|
+
}
|
|
137
|
+
try verifyStorageLocked()
|
|
138
|
+
try driver.setUserInfo(
|
|
139
|
+
NativeCrashBreadcrumbs.encoded(breadcrumbs, truncated: truncated),
|
|
140
|
+
forKey: NativeCrashBreadcrumbs.reportKey,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
109
144
|
@objc(replayPendingReportsWithHandler:error:)
|
|
110
145
|
public func replayPendingReports(
|
|
111
146
|
_ handler: (NativeCrashRecord) -> Bool,
|
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
enum NativeCrashContextState: String {
|
|
6
|
+
case captured
|
|
7
|
+
case notCaptured = "not_captured"
|
|
8
|
+
case unavailable
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
enum NativeCrashCorrelation {
|
|
12
|
+
static let reportKey = "logbrew_native_correlation"
|
|
13
|
+
private static let maximumBytes = 1024
|
|
14
|
+
|
|
15
|
+
static func encoded(_ context: TelemetryContext?) throws -> String? {
|
|
16
|
+
guard let context else {
|
|
17
|
+
return nil
|
|
18
|
+
}
|
|
19
|
+
do {
|
|
20
|
+
let data = try encoder().encode(validateNativeCrashCorrelationContext(context))
|
|
21
|
+
guard data.count <= maximumBytes, let value = String(data: data, encoding: .utf8) else {
|
|
22
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
23
|
+
}
|
|
24
|
+
return value
|
|
25
|
+
} catch {
|
|
26
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func captured(in rawReport: [String: Any]) -> (TelemetryContext?, NativeCrashContextState) {
|
|
31
|
+
guard let user = rawReport["user"] as? [String: Any], let value = user[reportKey] else {
|
|
32
|
+
return (nil, .notCaptured)
|
|
33
|
+
}
|
|
34
|
+
guard let value = value as? String,
|
|
35
|
+
let data = value.data(using: .utf8),
|
|
36
|
+
data.count <= maximumBytes,
|
|
37
|
+
let object = try? JSONSerialization.jsonObject(with: data),
|
|
38
|
+
let context = try? validated(object)
|
|
39
|
+
else {
|
|
40
|
+
return (nil, .unavailable)
|
|
41
|
+
}
|
|
42
|
+
return (context, .captured)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static func validated(_ object: Any) throws -> TelemetryContext {
|
|
46
|
+
do {
|
|
47
|
+
guard JSONSerialization.isValidJSONObject(object) else {
|
|
48
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
49
|
+
}
|
|
50
|
+
let data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
|
|
51
|
+
guard data.count <= maximumBytes else {
|
|
52
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
53
|
+
}
|
|
54
|
+
let context = try validateNativeCrashCorrelationContext(
|
|
55
|
+
JSONDecoder().decode(TelemetryContext.self, from: data),
|
|
56
|
+
)
|
|
57
|
+
let normalized = try encoder().encode(context)
|
|
58
|
+
let normalizedObject = try JSONSerialization.jsonObject(with: normalized)
|
|
59
|
+
guard object as? NSDictionary == normalizedObject as? NSDictionary else {
|
|
60
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
61
|
+
}
|
|
62
|
+
return context
|
|
63
|
+
} catch let error as NativeCrashError {
|
|
64
|
+
throw error
|
|
65
|
+
} catch {
|
|
66
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static func encoder() -> JSONEncoder {
|
|
71
|
+
let encoder = JSONEncoder()
|
|
72
|
+
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
|
|
73
|
+
return encoder
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
struct NativeCrashBreadcrumbSnapshot: Codable, Equatable {
|
|
78
|
+
let schemaVersion: Int
|
|
79
|
+
let breadcrumbs: [IssueBreadcrumb]
|
|
80
|
+
let truncated: Bool
|
|
81
|
+
|
|
82
|
+
init(breadcrumbs: [IssueBreadcrumb], truncated: Bool) {
|
|
83
|
+
schemaVersion = 1
|
|
84
|
+
self.breadcrumbs = breadcrumbs
|
|
85
|
+
self.truncated = truncated
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
enum NativeCrashBreadcrumbs {
|
|
90
|
+
static let reportKey = "logbrew_native_breadcrumbs"
|
|
91
|
+
static let maximumBytes = 64 * 1024
|
|
92
|
+
|
|
93
|
+
static func encoded(_ values: [IssueBreadcrumb]?, truncated: Bool) throws -> String? {
|
|
94
|
+
guard let values, !values.isEmpty else {
|
|
95
|
+
return nil
|
|
96
|
+
}
|
|
97
|
+
do {
|
|
98
|
+
var breadcrumbs = try values.suffix(maximumIssueBreadcrumbs).map(validateIssueBreadcrumb)
|
|
99
|
+
var wasTruncated = truncated || breadcrumbs.count < values.count
|
|
100
|
+
while !breadcrumbs.isEmpty {
|
|
101
|
+
let data = try encoder().encode(NativeCrashBreadcrumbSnapshot(
|
|
102
|
+
breadcrumbs: breadcrumbs,
|
|
103
|
+
truncated: wasTruncated,
|
|
104
|
+
))
|
|
105
|
+
if data.count <= maximumBytes, let value = String(data: data, encoding: .utf8) {
|
|
106
|
+
return value
|
|
107
|
+
}
|
|
108
|
+
breadcrumbs.removeFirst()
|
|
109
|
+
wasTruncated = true
|
|
110
|
+
}
|
|
111
|
+
} catch let error as NativeCrashError {
|
|
112
|
+
throw error
|
|
113
|
+
} catch {
|
|
114
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
115
|
+
}
|
|
116
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static func captured(
|
|
120
|
+
in rawReport: [String: Any],
|
|
121
|
+
) -> (NativeCrashBreadcrumbSnapshot?, NativeCrashContextState) {
|
|
122
|
+
guard let user = rawReport["user"] as? [String: Any], let value = user[reportKey] else {
|
|
123
|
+
return (nil, .notCaptured)
|
|
124
|
+
}
|
|
125
|
+
guard let value = value as? String,
|
|
126
|
+
let data = value.data(using: .utf8),
|
|
127
|
+
data.count <= maximumBytes,
|
|
128
|
+
let object = try? JSONSerialization.jsonObject(with: data),
|
|
129
|
+
let snapshot = try? validated(object)
|
|
130
|
+
else {
|
|
131
|
+
return (nil, .unavailable)
|
|
132
|
+
}
|
|
133
|
+
return (snapshot, .captured)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static func validated(_ object: Any) throws -> NativeCrashBreadcrumbSnapshot {
|
|
137
|
+
do {
|
|
138
|
+
guard JSONSerialization.isValidJSONObject(object) else {
|
|
139
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
140
|
+
}
|
|
141
|
+
let data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
|
|
142
|
+
guard data.count <= maximumBytes else {
|
|
143
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
144
|
+
}
|
|
145
|
+
let decoded = try JSONDecoder().decode(NativeCrashBreadcrumbSnapshot.self, from: data)
|
|
146
|
+
guard decoded.schemaVersion == 1,
|
|
147
|
+
!decoded.breadcrumbs.isEmpty,
|
|
148
|
+
decoded.breadcrumbs.count <= maximumIssueBreadcrumbs
|
|
149
|
+
else {
|
|
150
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
151
|
+
}
|
|
152
|
+
let snapshot = try NativeCrashBreadcrumbSnapshot(
|
|
153
|
+
breadcrumbs: decoded.breadcrumbs.map(validateIssueBreadcrumb),
|
|
154
|
+
truncated: decoded.truncated,
|
|
155
|
+
)
|
|
156
|
+
let normalized = try encoder().encode(snapshot)
|
|
157
|
+
let normalizedObject = try JSONSerialization.jsonObject(with: normalized)
|
|
158
|
+
guard object as? NSDictionary == normalizedObject as? NSDictionary else {
|
|
159
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
160
|
+
}
|
|
161
|
+
return snapshot
|
|
162
|
+
} catch let error as NativeCrashError {
|
|
163
|
+
throw error
|
|
164
|
+
} catch {
|
|
165
|
+
throw NativeCrashError(.invalidConfiguration)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private static func encoder() -> JSONEncoder {
|
|
170
|
+
let encoder = JSONEncoder()
|
|
171
|
+
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
|
|
172
|
+
return encoder
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -164,6 +164,10 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
164
164
|
private let nativeStackFrames: [NativeStackFrame]?
|
|
165
165
|
private let artifactIdentity: NativeArtifactIdentity?
|
|
166
166
|
private let context: TelemetryContext?
|
|
167
|
+
private let correlationState: NativeCrashContextState?
|
|
168
|
+
private let breadcrumbs: [IssueBreadcrumb]?
|
|
169
|
+
private let breadcrumbsTruncated: Bool?
|
|
170
|
+
private let breadcrumbState: NativeCrashContextState?
|
|
167
171
|
private let hangState: NativeHangIncidentState?
|
|
168
172
|
private let hangDurationMs: Double?
|
|
169
173
|
let source: NativeCrashRecordSource
|
|
@@ -177,6 +181,10 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
177
181
|
nativeStackFrames: [NativeStackFrame]?,
|
|
178
182
|
artifactIdentity: NativeArtifactIdentity?,
|
|
179
183
|
context: TelemetryContext?,
|
|
184
|
+
correlationState: NativeCrashContextState? = nil,
|
|
185
|
+
breadcrumbs: [IssueBreadcrumb]? = nil,
|
|
186
|
+
breadcrumbsTruncated: Bool? = nil,
|
|
187
|
+
breadcrumbState: NativeCrashContextState? = nil,
|
|
180
188
|
hangState: NativeHangIncidentState?,
|
|
181
189
|
hangDurationMs: Double? = nil,
|
|
182
190
|
source: NativeCrashRecordSource,
|
|
@@ -189,6 +197,10 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
189
197
|
self.nativeStackFrames = nativeStackFrames
|
|
190
198
|
self.artifactIdentity = artifactIdentity
|
|
191
199
|
self.context = context
|
|
200
|
+
self.correlationState = correlationState
|
|
201
|
+
self.breadcrumbs = breadcrumbs
|
|
202
|
+
self.breadcrumbsTruncated = breadcrumbsTruncated
|
|
203
|
+
self.breadcrumbState = breadcrumbState
|
|
192
204
|
self.hangState = hangState
|
|
193
205
|
self.hangDurationMs = hangDurationMs
|
|
194
206
|
self.source = source
|
|
@@ -228,6 +240,12 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
228
240
|
metadata["environment"] = .string(artifactIdentity.environment)
|
|
229
241
|
metadata["service"] = .string(artifactIdentity.service)
|
|
230
242
|
}
|
|
243
|
+
if let correlationState {
|
|
244
|
+
metadata["crash.correlation"] = .string(correlationState.rawValue)
|
|
245
|
+
}
|
|
246
|
+
if let breadcrumbState {
|
|
247
|
+
metadata["crash.breadcrumbs"] = .string(breadcrumbState.rawValue)
|
|
248
|
+
}
|
|
231
249
|
if let hangState {
|
|
232
250
|
metadata["crash.handled"] = .bool(hangState == .recovered)
|
|
233
251
|
}
|
|
@@ -239,6 +257,8 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
239
257
|
level: hangState == .recovered ? .error : .fatal,
|
|
240
258
|
exception: issueException,
|
|
241
259
|
exceptionChain: nativeCrashExceptionChain(for: issueException),
|
|
260
|
+
breadcrumbs: breadcrumbs,
|
|
261
|
+
breadcrumbsTruncated: breadcrumbsTruncated,
|
|
242
262
|
metadata: metadata,
|
|
243
263
|
context: context,
|
|
244
264
|
nativeStackFrames: nativeStackFrames,
|
|
@@ -281,9 +301,18 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
|
|
|
281
301
|
else {
|
|
282
302
|
return false
|
|
283
303
|
}
|
|
284
|
-
|
|
304
|
+
let legacyFields = ["exception", "exceptionChain", "breadcrumbs", "breadcrumbsTruncated", "context"]
|
|
305
|
+
for legacyField in legacyFields where actual[legacyField] == nil {
|
|
285
306
|
expected.removeValue(forKey: legacyField)
|
|
286
307
|
}
|
|
308
|
+
let actualMetadata = actual["metadata"] as? [String: Any]
|
|
309
|
+
if actualMetadata?["crash.correlation"] == nil || actualMetadata?["crash.breadcrumbs"] == nil {
|
|
310
|
+
var metadata = expected["metadata"] as? [String: Any]
|
|
311
|
+
for key in ["crash.correlation", "crash.breadcrumbs"] where actualMetadata?[key] == nil {
|
|
312
|
+
metadata?.removeValue(forKey: key)
|
|
313
|
+
}
|
|
314
|
+
expected["metadata"] = metadata
|
|
315
|
+
}
|
|
287
316
|
return actual as NSDictionary == expected as NSDictionary
|
|
288
317
|
}
|
|
289
318
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"swift/logbrew-swift/Sources/LogBrew/DurableDeliveryStoreRecovery.swift": "79fee27975b6571954bf0b3692a50e60bbbcf53b5ab47f653a474db5c56d68ba",
|
|
12
12
|
"swift/logbrew-swift/Sources/LogBrew/EventEncoding.swift": "b88b6b2d17f7d0cee9ac1a1f34ef35b192cd33d213ef3c9e1c2d21cbcc304a97",
|
|
13
13
|
"swift/logbrew-swift/Sources/LogBrew/IssueDiagnosticEvidence.swift": "e471f27fadaefe06971c1a98892aeb0774e840a2ce0cbb243ae9aabfa41b1c47",
|
|
14
|
-
"swift/logbrew-swift/Sources/LogBrew/IssueDiagnostics.swift": "
|
|
14
|
+
"swift/logbrew-swift/Sources/LogBrew/IssueDiagnostics.swift": "bbdfe6169961ff27b8f2cee63c7dcdeacf2e5cee6a3ca17f6a26b933f0adfc39",
|
|
15
15
|
"swift/logbrew-swift/Sources/LogBrew/IssueExceptionChain.swift": "10f81cbce95b47da0a44ef9e515b150e60d71dfcc8b04be24b037b1222a373a5",
|
|
16
16
|
"swift/logbrew-swift/Sources/LogBrew/LifecycleTrace.swift": "738cd3f129fac821a892404d1d42ee92bd806b6583d3dbb5379e88b14ab23c16",
|
|
17
17
|
"swift/logbrew-swift/Sources/LogBrew/LogBrewClient.swift": "186cdbda3754a811f17b82818ca8e7a1711b3b75123686cd17dc3b7441fa21cb",
|
|
@@ -22,19 +22,20 @@
|
|
|
22
22
|
"swift/logbrew-swift/Sources/LogBrew/ProductTimeline.swift": "209906aa0e8f096a347e35832840522710eadfb530053253cd73c748518e362d",
|
|
23
23
|
"swift/logbrew-swift/Sources/LogBrew/PublicTypes.swift": "a21aee058f716ec9d41d54ad032b2c7a4d64a4b99d749a9fa2be3fd2df5c6568",
|
|
24
24
|
"swift/logbrew-swift/Sources/LogBrew/TelemetryContext.swift": "ce3ac93a62a345f8c9a8f4c69ce84efc991bf02a688f49bad5e68edcff2f6e08",
|
|
25
|
-
"swift/logbrew-swift/Sources/LogBrew/TelemetryContextValidation.swift": "
|
|
25
|
+
"swift/logbrew-swift/Sources/LogBrew/TelemetryContextValidation.swift": "ab5811b48df308bf8f6435efee30a24f91a62bfe8b6dea612a2077c8ae0eb302",
|
|
26
26
|
"swift/logbrew-swift/Sources/LogBrew/TraceEvidence.swift": "40aaa42976164c53d9f4d9e9e30d236534238da43bbe90832a673c8809ad54b6",
|
|
27
27
|
"swift/logbrew-swift/Sources/LogBrew/Transport.swift": "73eec81aaebec4bc922b2a372429c88fcd3e85606a7862cd08e1795eb2a8e6cd",
|
|
28
28
|
"swift/logbrew-swift/Sources/LogBrew/URLSessionTrace.swift": "d0076c72671716c41bd7498d0509e453b8182c418156e169196fee9f26fc66b1",
|
|
29
29
|
"swift/logbrew-swift/Sources/LogBrew/URLSessionTracer.swift": "0cdf08f92d6111c0dc2883b87837b81f69b5c2f9a8e5501780a114c23a741af7",
|
|
30
30
|
"swift/logbrew-swift/Sources/LogBrew/Validation.swift": "07ff5c2daeb829b1df4f50edb0b79727f9a81b1c9b5cde5f995f5ad6ce660aa7",
|
|
31
|
-
"swift/logbrew-swift/Sources/LogBrewCrash/CrashEngine.swift": "
|
|
32
|
-
"swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "
|
|
31
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/CrashEngine.swift": "a94dfce653e3e7f381be42b6d2c4540f0be77e83777ecc53e06682cd2e569c5a",
|
|
32
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "ccd0ef83f14096816765278e007f5e4ba0a40286ce00d708b38ca0b94959171e",
|
|
33
33
|
"swift/logbrew-swift/Sources/LogBrewCrash/CrashStorageDirectory.swift": "7cd566703cbf704dc99155451ee93dcace12c35c805abc09a6dcf23975cf43f9",
|
|
34
34
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeArtifactIdentity.swift": "3d785ad717dcf7302451531c18b5e1183983270a6cd78dd543a407e3facc6ae0",
|
|
35
|
-
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCapture.swift": "
|
|
35
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCapture.swift": "c2f5d0e9a8953cb988633ebf60a06f3b7ddaa0c9828f99372969fdfb60b99bc4",
|
|
36
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "eef19c6e561bc03bb4c09736722afe57f63d557f9279b26353679bd53362c25d",
|
|
36
37
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashExceptionEvidence.swift": "ce191e46298d91d87b379528d610caa88b924b15a8dd912b4a164b51eb172a7a",
|
|
37
|
-
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "
|
|
38
|
+
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "2d0f2e7cdce09dc09906cede5d83928e8bd0e70a8522ff55c999d19366d84730",
|
|
38
39
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashReplay.swift": "3eb3d93b32cf14de416f56fa30d6d65e6f268b5ec9d1a67081d7525c4efd4f80",
|
|
39
40
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeHangIncidentStore.swift": "5a8150083f16d6b495c9c63434809dc5f11f13ebb23211f57151bad62040781b",
|
|
40
41
|
"swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdog.swift": "37b25904cd6576e20c40500be2c153156403b57ad7b4186a1b6b35b414145ecb",
|
package/metadata.js
CHANGED
|
@@ -1,181 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
].join("|"), "u");
|
|
12
|
-
const REACT_NATIVE_DEBUG_ID_REGISTRY = Symbol.for("@logbrew/react-native/debug-ids");
|
|
13
|
-
const SAFE_RELEASE_ARTIFACT_DEBUG_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
14
|
-
const MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_ENTRIES = 64;
|
|
15
|
-
const MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_FRAMES = 128;
|
|
16
|
-
|
|
17
|
-
export function createSafeReactNativeMetadata(metadata, metadataFactory, context) {
|
|
18
|
-
if (typeof metadataFactory !== "function") {
|
|
19
|
-
return metadata;
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
...metadata,
|
|
23
|
-
...safeReactNativeMetadataFactoryResult(metadataFactory(context))
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function safeReactNativeMetadataFactoryResult(candidate) {
|
|
28
|
-
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
const metadata = {};
|
|
32
|
-
for (const [key, value] of Object.entries(candidate)) {
|
|
33
|
-
if (isSensitiveMetadataKey(key)) {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
37
|
-
metadata[key] = value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return metadata;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function sanitizeReactNativeIssueMetadata(metadata, compactMetadata) {
|
|
44
|
-
const next = { ...metadata };
|
|
45
|
-
for (const key of ["errorFrameFile", "releaseArtifactCodeFile"]) {
|
|
46
|
-
const path = reactNativeCodePath(next[key]);
|
|
47
|
-
if (path) {
|
|
48
|
-
next[key] = path;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const match = typeof next.issueGroupingKey === "string" ? next.issueGroupingKey.match(/^([^:]+):([^:]+):(.+)$/u) : null;
|
|
52
|
-
const path = match ? reactNativeCodePath(match[3]) : undefined;
|
|
53
|
-
if (path) {
|
|
54
|
-
next.issueGroupingKey = `${match[1]}:${match[2]}:${path}`;
|
|
55
|
-
}
|
|
56
|
-
return compactMetadata(next);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function sanitizeReactNativeIssueStackFrames(stackFrames) {
|
|
60
|
-
if (!Array.isArray(stackFrames)) {
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
return stackFrames.map((frame) => ({
|
|
64
|
-
...frame,
|
|
65
|
-
filename: reactNativeCodePath(frame.filename) ?? frame.filename
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function sanitizeReactNativeIssueExceptionChain(exceptionChain) {
|
|
70
|
-
if (!exceptionChain || !Array.isArray(exceptionChain.entries)) {
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
...exceptionChain,
|
|
75
|
-
entries: exceptionChain.entries.map((entry) => {
|
|
76
|
-
const stackFrames = sanitizeReactNativeIssueStackFrames(entry.stackFrames);
|
|
77
|
-
return {
|
|
78
|
-
...entry,
|
|
79
|
-
...(stackFrames ? { stackFrames } : {})
|
|
80
|
-
};
|
|
81
|
-
})
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function runtimeReactNativeDebugIdMap() {
|
|
86
|
-
try {
|
|
87
|
-
const registry = globalThis?.[REACT_NATIVE_DEBUG_ID_REGISTRY];
|
|
88
|
-
if (!registry || Array.isArray(registry) || typeof registry !== "object") {
|
|
89
|
-
return undefined;
|
|
90
|
-
}
|
|
91
|
-
const entries = Object.entries(registry);
|
|
92
|
-
if (entries.length === 0 || entries.length > MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_ENTRIES) {
|
|
93
|
-
return undefined;
|
|
94
|
-
}
|
|
95
|
-
const debugIdMap = Object.create(null);
|
|
96
|
-
let frameCount = 0;
|
|
97
|
-
for (const [stack, debugId] of entries) {
|
|
98
|
-
if (typeof debugId !== "string" || !SAFE_RELEASE_ARTIFACT_DEBUG_ID.test(debugId)) {
|
|
99
|
-
return undefined;
|
|
100
|
-
}
|
|
101
|
-
const normalizedDebugId = debugId.toLowerCase();
|
|
102
|
-
let stackFrameCount = 0;
|
|
103
|
-
for (const line of stack.split(/\r?\n/u)) {
|
|
104
|
-
const filename = runtimeStackFrameFilename(line);
|
|
105
|
-
if (!filename) {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
frameCount += 1;
|
|
109
|
-
stackFrameCount += 1;
|
|
110
|
-
if (frameCount > MAX_REACT_NATIVE_DEBUG_ID_REGISTRY_FRAMES) {
|
|
111
|
-
return undefined;
|
|
112
|
-
}
|
|
113
|
-
const existingDebugId = debugIdMap[filename];
|
|
114
|
-
if (existingDebugId && existingDebugId !== normalizedDebugId) {
|
|
115
|
-
return undefined;
|
|
116
|
-
}
|
|
117
|
-
debugIdMap[filename] = normalizedDebugId;
|
|
118
|
-
}
|
|
119
|
-
if (stackFrameCount === 0) {
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return frameCount > 0 ? debugIdMap : undefined;
|
|
124
|
-
} catch {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function isSensitiveMetadataKey(key) {
|
|
130
|
-
return SENSITIVE_METADATA_FACTORY_KEY_RE.test(String(key).toLowerCase());
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function runtimeStackFrameFilename(rawLine) {
|
|
134
|
-
let location = typeof rawLine === "string" ? rawLine.trim() : "";
|
|
135
|
-
if (!location) {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
if (location.startsWith("at ")) {
|
|
139
|
-
location = location.slice(3).trim();
|
|
140
|
-
if (location.endsWith(")") && location.includes("(")) {
|
|
141
|
-
location = location.slice(location.lastIndexOf("(") + 1, -1);
|
|
142
|
-
}
|
|
143
|
-
} else if (location.includes("@")) {
|
|
144
|
-
location = location.slice(location.lastIndexOf("@") + 1);
|
|
145
|
-
}
|
|
146
|
-
const parts = location.split(":");
|
|
147
|
-
if (parts.length < 3) {
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
const columnText = parts.pop();
|
|
151
|
-
const lineText = parts.pop();
|
|
152
|
-
const filename = parts.join(":").trim();
|
|
153
|
-
if (!/^[1-9]\d*$/u.test(lineText) || !/^[1-9]\d*$/u.test(columnText)) {
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
const line = Number(lineText);
|
|
157
|
-
const column = Number(columnText);
|
|
158
|
-
return Number.isSafeInteger(line) && Number.isSafeInteger(column) && filename ? filename : undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function reactNativeCodePath(value) {
|
|
162
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
163
|
-
return undefined;
|
|
164
|
-
}
|
|
165
|
-
let path = value.trim();
|
|
166
|
-
const URLConstructor = globalThis.URL;
|
|
167
|
-
if (typeof URLConstructor === "function") {
|
|
168
|
-
try {
|
|
169
|
-
path = new URLConstructor(path).pathname || path;
|
|
170
|
-
} catch {
|
|
171
|
-
path = path.split(/[?#]/u, 1)[0].replace(/\\/g, "/");
|
|
172
|
-
}
|
|
173
|
-
} else {
|
|
174
|
-
path = path.split(/[?#]/u, 1)[0].replace(/\\/g, "/");
|
|
175
|
-
}
|
|
176
|
-
if (/^[A-Za-z]:\//u.test(path) || /^\/(?:Users|home|private|tmp|var)\//u.test(path)) {
|
|
177
|
-
path = path.replace(/\/+$/u, "");
|
|
178
|
-
return path.slice(path.lastIndexOf("/") + 1) || undefined;
|
|
179
|
-
}
|
|
180
|
-
return path || undefined;
|
|
181
|
-
}
|
|
1
|
+
import implementation from "./metadata.cjs";
|
|
2
|
+
|
|
3
|
+
export const {
|
|
4
|
+
createSafeReactNativeMetadata,
|
|
5
|
+
runtimeReactNativeDebugIdMap,
|
|
6
|
+
safeReactNativeMetadataFactoryResult,
|
|
7
|
+
sanitizeReactNativeIssueExceptionChain,
|
|
8
|
+
sanitizeReactNativeIssueMetadata,
|
|
9
|
+
sanitizeReactNativeIssueStackFrames
|
|
10
|
+
} = implementation;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logbrew/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "React Native offline delivery, Apple native diagnostics, screen, error, trace, action, and network timeline helpers for LogBrew.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"url": "git+https://github.com/LogBrewCo/sdk.git"
|
|
241
241
|
},
|
|
242
242
|
"peerDependencies": {
|
|
243
|
-
"@logbrew/sdk": "^0.1.
|
|
243
|
+
"@logbrew/sdk": "^0.1.15",
|
|
244
244
|
"expo": ">=49",
|
|
245
245
|
"react": ">=18",
|
|
246
246
|
"react-native": ">=0.72"
|