@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.
@@ -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
- func validateIssueBreadcrumb(_ value: IssueBreadcrumb) throws -> IssueBreadcrumb {
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
- guard artifactIdentity != nil || operatingSystem != nil || device != nil || application != nil else {
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(resource: TelemetryResource(
101
- service: artifactIdentity.map { TelemetryNamedVersion(name: $0.service) },
102
- deployment: artifactIdentity.map {
103
- TelemetryDeployment(environment: $0.environment, release: $0.release)
104
- },
105
- operatingSystem: operatingSystem,
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
- for legacyField in ["exception", "exceptionChain", "context"] where actual[legacyField] == nil {
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": "297b8bbefd42c314a984fecb38237fb1395700c9eafcfdd1b545267bc34294cc",
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": "23524d281a185962a2ecc75f1ca1cb775171e2f72ecd39ca325ace7e45082485",
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": "7a46035fcaa72dd7504b4e274a92070d73dd1c45fe20e008474340d6d7c44ede",
32
- "swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "8798c8900ea28a85d6909183dc8435bc08da96573b89310d26ed178217fbc04b",
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": "e9ab5f1850a096f40592a025d16c5ffa2fdff0222004a37056898cf1cb4b0dd9",
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": "7c1d028bea143c6ab34ebddaaaab64aeb69d4d950ec7a42264c567e672991748",
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
- const SENSITIVE_METADATA_FACTORY_KEY_RE = new RegExp([
2
- "body",
3
- "payload",
4
- "variable",
5
- "header",
6
- "authorization",
7
- "cookie",
8
- "to\u006ben",
9
- "sec\u0072et",
10
- "pass\u0077ord"
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.19",
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.12",
243
+ "@logbrew/sdk": "^0.1.15",
244
244
  "expo": ">=49",
245
245
  "react": ">=18",
246
246
  "react-native": ">=0.72"