@logbrew/react-native 0.1.19 → 0.1.20

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/index.native.d.ts CHANGED
@@ -46,6 +46,7 @@ declare const defaultExport: Omit<
46
46
  installLogBrewAppleNativeDiagnostics: typeof import("./apple-native-diagnostics.js").installLogBrewAppleNativeDiagnostics;
47
47
  purgeLogBrewReactNativePersistentQueue: typeof purgeLogBrewReactNativePersistentQueue;
48
48
  replayLogBrewAppleNativeDiagnostics: typeof import("./apple-native-diagnostics.js").replayLogBrewAppleNativeDiagnostics;
49
+ setLogBrewAppleNativeCrashContext: typeof import("./apple-native-diagnostics.js").setLogBrewAppleNativeCrashContext;
49
50
  };
50
51
 
51
52
  export default defaultExport;
package/index.native.js CHANGED
@@ -13,7 +13,8 @@ import baseDefault from "./index.js";
13
13
  import {
14
14
  getLogBrewAppleNativeDiagnosticsStatus,
15
15
  installLogBrewAppleNativeDiagnostics,
16
- replayLogBrewAppleNativeDiagnostics
16
+ replayLogBrewAppleNativeDiagnostics,
17
+ setLogBrewAppleNativeCrashContext
17
18
  } from "./apple-native-diagnostics.js";
18
19
  import {
19
20
  purgeReactNativePersistentQueue,
@@ -26,7 +27,8 @@ export {
26
27
  export {
27
28
  getLogBrewAppleNativeDiagnosticsStatus,
28
29
  installLogBrewAppleNativeDiagnostics,
29
- replayLogBrewAppleNativeDiagnostics
30
+ replayLogBrewAppleNativeDiagnostics,
31
+ setLogBrewAppleNativeCrashContext
30
32
  };
31
33
 
32
34
  export * from "./index.js";
@@ -146,7 +148,8 @@ const defaultExport = {
146
148
  getLogBrewAppleNativeDiagnosticsStatus,
147
149
  installLogBrewAppleNativeDiagnostics,
148
150
  purgeLogBrewReactNativePersistentQueue,
149
- replayLogBrewAppleNativeDiagnostics
151
+ replayLogBrewAppleNativeDiagnostics,
152
+ setLogBrewAppleNativeCrashContext
150
153
  };
151
154
 
152
155
  export default defaultExport;
@@ -40,6 +40,11 @@ RCT_EXPORT_METHOD(replayNativeDiagnostics:(RCTPromiseResolveBlock)resolve
40
40
  }];
41
41
  }
42
42
 
43
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(setNativeDiagnosticsContext:(NSDictionary *)context)
44
+ {
45
+ return [LBRNAppleNativeDiagnostics setCorrelationContext:context];
46
+ }
47
+
43
48
  RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(nativeDiagnosticsStatus)
44
49
  {
45
50
  return [LBRNAppleNativeDiagnostics status];
@@ -3,7 +3,7 @@ import Foundation
3
3
  @objc(LBRNAppleNativeDiagnostics)
4
4
  public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
5
5
  private static let shared = LBRNAppleNativeDiagnostics()
6
- private static let sdkVersion = "0.1.19"
6
+ private static let sdkVersion = "0.1.20"
7
7
 
8
8
  private let lock = NSLock()
9
9
  private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
@@ -39,6 +39,11 @@ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
39
39
  shared.currentStatus()
40
40
  }
41
41
 
42
+ @objc(setCorrelationContext:)
43
+ public static func setCorrelationContext(_ rawContext: NSDictionary?) -> NSDictionary {
44
+ shared.updateCorrelationContext(rawContext)
45
+ }
46
+
42
47
  private func installOnMainThread(_ rawConfiguration: NSDictionary) -> NSDictionary {
43
48
  guard let configuration = InstalledConfiguration(rawConfiguration) else {
44
49
  return failure("native_diagnostics_invalid_configuration")
@@ -132,6 +137,23 @@ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
132
137
  return captureStatus(status: "ready")
133
138
  }
134
139
 
140
+ private func updateCorrelationContext(_ rawContext: NSDictionary?) -> NSDictionary {
141
+ lock.lock()
142
+ defer { lock.unlock() }
143
+ guard let capture else {
144
+ return failure("native_diagnostics_not_installed")
145
+ }
146
+ do {
147
+ let context = try rawContext.map { try NativeCrashCorrelation.validated($0) }
148
+ try capture.setCorrelationContext(context)
149
+ return ["status": rawContext == nil ? "cleared" : "updated"]
150
+ } catch let error as NativeCrashError {
151
+ return failure(error.code.rawValue)
152
+ } catch {
153
+ return failure("native_diagnostics_failed")
154
+ }
155
+ }
156
+
135
157
  private func captureStatus(status: String) -> NSDictionary {
136
158
  guard let capture else {
137
159
  return failure("native_diagnostics_not_installed")
@@ -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,7 @@ 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)
25
26
  return NativeCrashRecord(
26
27
  eventID: uuid.uuidString.lowercased(),
27
28
  timestamp: timestamp.normalized,
@@ -31,7 +32,9 @@ struct CrashReportSanitizer {
31
32
  context: nativeCrashContext(
32
33
  system: rawReport["system"] as? [String: Any],
33
34
  artifactIdentity: artifactIdentity,
35
+ correlation: correlation.0,
34
36
  ),
37
+ correlationState: correlation.1,
35
38
  hangState: nil,
36
39
  hangDurationMs: nil,
37
40
  source: .engine(reportID: reportID),
@@ -77,6 +80,7 @@ struct CrashReportSanitizer {
77
80
  func nativeCrashContext(
78
81
  system: [String: Any]?,
79
82
  artifactIdentity: NativeArtifactIdentity?,
83
+ correlation: TelemetryContext? = nil,
80
84
  ) -> TelemetryContext? {
81
85
  let value = { (key: String) in crashContextValue(system?[key]) }
82
86
  let operatingSystem = value("system_name").map {
@@ -94,18 +98,26 @@ func nativeCrashContext(
94
98
  let application = applicationName == nil && applicationVersion == nil && applicationBuild == nil
95
99
  ? nil
96
100
  : TelemetryApplication(name: applicationName, version: applicationVersion, build: applicationBuild)
97
- guard artifactIdentity != nil || operatingSystem != nil || device != nil || application != nil else {
101
+ let resource = artifactIdentity != nil || operatingSystem != nil || device != nil || application != nil
102
+ ? TelemetryResource(
103
+ service: artifactIdentity.map { TelemetryNamedVersion(name: $0.service) },
104
+ deployment: artifactIdentity.map {
105
+ TelemetryDeployment(environment: $0.environment, release: $0.release)
106
+ },
107
+ operatingSystem: operatingSystem,
108
+ device: device,
109
+ application: application,
110
+ )
111
+ : nil
112
+ guard resource != nil || correlation != nil else {
98
113
  return nil
99
114
  }
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
- ))
115
+ return TelemetryContext(
116
+ resource: resource,
117
+ trace: correlation?.trace,
118
+ session: correlation?.session,
119
+ subject: correlation?.subject,
120
+ )
109
121
  }
110
122
 
111
123
  private func crashContextValue(_ rawValue: Any?) -> String? {
@@ -106,6 +106,22 @@ 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
+
109
125
  @objc(replayPendingReportsWithHandler:error:)
110
126
  public func replayPendingReports(
111
127
  _ handler: (NativeCrashRecord) -> Bool,
@@ -0,0 +1,77 @@
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 NativeCrashCorrelationState: 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 let error as NativeCrashError {
26
+ throw error
27
+ } catch {
28
+ throw NativeCrashError(.invalidConfiguration)
29
+ }
30
+ }
31
+
32
+ static func captured(in rawReport: [String: Any]) -> (TelemetryContext?, NativeCrashCorrelationState) {
33
+ guard let user = rawReport["user"] as? [String: Any], let value = user[reportKey] else {
34
+ return (nil, .notCaptured)
35
+ }
36
+ guard let value = value as? String,
37
+ let data = value.data(using: .utf8),
38
+ data.count <= maximumBytes,
39
+ let object = try? JSONSerialization.jsonObject(with: data),
40
+ let context = try? validated(object)
41
+ else {
42
+ return (nil, .unavailable)
43
+ }
44
+ return (context, .captured)
45
+ }
46
+
47
+ static func validated(_ object: Any) throws -> TelemetryContext {
48
+ do {
49
+ guard JSONSerialization.isValidJSONObject(object) else {
50
+ throw NativeCrashError(.invalidConfiguration)
51
+ }
52
+ let data = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
53
+ guard data.count <= maximumBytes else {
54
+ throw NativeCrashError(.invalidConfiguration)
55
+ }
56
+ let context = try validateNativeCrashCorrelationContext(
57
+ JSONDecoder().decode(TelemetryContext.self, from: data),
58
+ )
59
+ let normalized = try encoder().encode(context)
60
+ let normalizedObject = try JSONSerialization.jsonObject(with: normalized)
61
+ guard object as? NSDictionary == normalizedObject as? NSDictionary else {
62
+ throw NativeCrashError(.invalidConfiguration)
63
+ }
64
+ return context
65
+ } catch let error as NativeCrashError {
66
+ throw error
67
+ } catch {
68
+ throw NativeCrashError(.invalidConfiguration)
69
+ }
70
+ }
71
+
72
+ private static func encoder() -> JSONEncoder {
73
+ let encoder = JSONEncoder()
74
+ encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
75
+ return encoder
76
+ }
77
+ }
@@ -164,6 +164,7 @@ 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: NativeCrashCorrelationState?
167
168
  private let hangState: NativeHangIncidentState?
168
169
  private let hangDurationMs: Double?
169
170
  let source: NativeCrashRecordSource
@@ -177,6 +178,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
177
178
  nativeStackFrames: [NativeStackFrame]?,
178
179
  artifactIdentity: NativeArtifactIdentity?,
179
180
  context: TelemetryContext?,
181
+ correlationState: NativeCrashCorrelationState? = nil,
180
182
  hangState: NativeHangIncidentState?,
181
183
  hangDurationMs: Double? = nil,
182
184
  source: NativeCrashRecordSource,
@@ -189,6 +191,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
189
191
  self.nativeStackFrames = nativeStackFrames
190
192
  self.artifactIdentity = artifactIdentity
191
193
  self.context = context
194
+ self.correlationState = correlationState
192
195
  self.hangState = hangState
193
196
  self.hangDurationMs = hangDurationMs
194
197
  self.source = source
@@ -228,6 +231,9 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
228
231
  metadata["environment"] = .string(artifactIdentity.environment)
229
232
  metadata["service"] = .string(artifactIdentity.service)
230
233
  }
234
+ if let correlationState {
235
+ metadata["crash.correlation"] = .string(correlationState.rawValue)
236
+ }
231
237
  if let hangState {
232
238
  metadata["crash.handled"] = .bool(hangState == .recovered)
233
239
  }
@@ -284,6 +290,11 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
284
290
  for legacyField in ["exception", "exceptionChain", "context"] where actual[legacyField] == nil {
285
291
  expected.removeValue(forKey: legacyField)
286
292
  }
293
+ if (actual["metadata"] as? [String: Any])?["crash.correlation"] == nil {
294
+ var metadata = expected["metadata"] as? [String: Any]
295
+ metadata?.removeValue(forKey: "crash.correlation")
296
+ expected["metadata"] = metadata
297
+ }
287
298
  return actual as NSDictionary == expected as NSDictionary
288
299
  }
289
300
  }
@@ -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": "5b503240977bd0c266035b2024e16c50846e6d2932dea083182bc549192a804f",
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": "3ebecef96e67b8e5a56a10d0d847470cb276dea44164e2e637584e110ea3ea39",
36
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "a513d933eea0b4e83418eb5dd60086cc6871c9e0147a6e7224c45cc6b502de2e",
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": "1672116a06fbdd10971838817591ee23af68bd5a9b07bbffb073e0fafc6cb1c0",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/react-native",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
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",
@@ -6,6 +6,9 @@ export interface Spec extends TurboModule {
6
6
  configuration: CodegenTypes.UnsafeObject
7
7
  ): CodegenTypes.UnsafeObject;
8
8
  replayNativeDiagnostics(): Promise<CodegenTypes.UnsafeObject>;
9
+ setNativeDiagnosticsContext(
10
+ context: CodegenTypes.UnsafeObject | null
11
+ ): CodegenTypes.UnsafeObject;
9
12
  nativeDiagnosticsStatus(): CodegenTypes.UnsafeObject;
10
13
  }
11
14