@logbrew/react-native 0.1.21 → 0.1.22

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 CHANGED
@@ -327,7 +327,11 @@ setLogBrewAppleNativeCrashContext({
327
327
  schemaVersion: 1,
328
328
  trace: { traceId: trace.traceId, spanId: trace.spanId, sampled: trace.sampled },
329
329
  session: { id: "session_123" },
330
- subject: { id: "subject_456", kind: "user" }
330
+ subject: { id: "subject_456", kind: "user" },
331
+ impact: {
332
+ failedAction: "checkout.submit",
333
+ userVisibleOutcome: "The order was not confirmed."
334
+ }
331
335
  });
332
336
 
333
337
  void replayLogBrewAppleNativeDiagnostics().catch((error) => {
@@ -342,10 +346,15 @@ allowed, but only one integration may install native fatal capture in a given
342
346
  process. LogBrew cannot transfer or remove that ownership before process
343
347
  restart.
344
348
 
345
- Update this snapshot when the active trace, session, or subject changes, and
349
+ Update this snapshot when the active trace, session, subject, or failed action changes, and
346
350
  clear it with `setLogBrewAppleNativeCrashContext(null)` on logout or session
347
351
  end. The crash report keeps one atomic snapshot from the crashed process, so a
348
- later app launch cannot replace it. Session and subject values must be opaque app-owned identifiers made from ASCII letters, numbers, `_`, or `-`. Do not use names, email addresses, IP addresses, or device identifiers. Resource context, tags, arbitrary fields, and values over the fixed 1 KiB snapshot limit fail before storage.
352
+ later app launch cannot replace it. `impact` is explicit app knowledge. Use a
353
+ stable action name and an optional safe user-visible result. Never include raw
354
+ input, request data, authentication values, or identity. Session and subject values must
355
+ be opaque app-owned identifiers made from ASCII letters, numbers, `_`, or `-`.
356
+ Resource context, tags, arbitrary fields, and values over the fixed 4 KiB
357
+ snapshot limit fail before storage.
349
358
 
350
359
  The iOS `createLogBrewReactNativeClient()` entry also mirrors its validated
351
360
  breadcrumb history into native crash capture. Install Apple diagnostics first,
@@ -36,6 +36,11 @@ export type LogBrewAppleNativeDiagnosticsReplayResult = Readonly<{
36
36
 
37
37
  export type LogBrewAppleNativeCrashContext = {
38
38
  schemaVersion: 1;
39
+ /** App-reported failed action and optional user-visible result. Never include raw input or identity. */
40
+ impact?: {
41
+ failedAction: string;
42
+ userVisibleOutcome?: string;
43
+ };
39
44
  trace?: TelemetryTraceContext;
40
45
  session?: TelemetrySessionContext;
41
46
  subject?: TelemetrySubjectContext;
@@ -144,7 +144,11 @@ function normalizeConfiguration(configuration) {
144
144
  }
145
145
 
146
146
  function normalizeCorrelationContext(context) {
147
- const source = exactObject(context, ["schemaVersion", "session", "subject", "trace"], "context");
147
+ const source = exactObject(
148
+ context,
149
+ ["impact", "schemaVersion", "session", "subject", "trace"],
150
+ "context"
151
+ );
148
152
  if (source.schemaVersion !== 1) {
149
153
  throw configurationError("context schemaVersion must be 1");
150
154
  }
@@ -185,8 +189,26 @@ function normalizeCorrelationContext(context) {
185
189
  }
186
190
  output.subject = { id: opaqueCorrelationId(subject.id, "subject id"), kind: subject.kind };
187
191
  }
192
+ if (source.impact !== undefined) {
193
+ const impact = exactObject(
194
+ source.impact,
195
+ ["failedAction", "userVisibleOutcome"],
196
+ "context impact"
197
+ );
198
+ output.impact = {
199
+ failedAction: diagnosticText(impact.failedAction, 256, true, "failedAction")
200
+ };
201
+ if (impact.userVisibleOutcome !== undefined) {
202
+ output.impact.userVisibleOutcome = diagnosticText(
203
+ impact.userVisibleOutcome,
204
+ 512,
205
+ false,
206
+ "userVisibleOutcome"
207
+ );
208
+ }
209
+ }
188
210
  if (Object.keys(output).length === 1) {
189
- throw configurationError("context must include trace, session, or subject");
211
+ throw configurationError("context must include trace, session, subject, or impact");
190
212
  }
191
213
  return output;
192
214
  }
@@ -218,6 +240,17 @@ function opaqueCorrelationId(value, name) {
218
240
  return value;
219
241
  }
220
242
 
243
+ function diagnosticText(value, maximum, rejectLocationText, name) {
244
+ const normalized = typeof value === "string" ? value.trim() : "";
245
+ if (normalized.length === 0
246
+ || Array.from(normalized).length > maximum
247
+ || hasControlCharacter(normalized)
248
+ || (rejectLocationText && /[?#]/u.test(normalized))) {
249
+ throw configurationError(`context impact ${name} is invalid`);
250
+ }
251
+ return normalized;
252
+ }
253
+
221
254
  function requireApplePlatform() {
222
255
  if (Platform?.OS !== "ios") {
223
256
  throw new SdkError(
@@ -366,7 +399,7 @@ function exactDeliveryEndpoint(value) {
366
399
  function hasControlCharacter(value) {
367
400
  for (const scalar of value) {
368
401
  const codePoint = scalar.codePointAt(0);
369
- if (codePoint <= 0x1f || codePoint === 0x7f) {
402
+ if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) {
370
403
  return true;
371
404
  }
372
405
  }
package/index.cjs CHANGED
@@ -16,7 +16,7 @@ const {
16
16
  } = require("./metadata.cjs");
17
17
 
18
18
  const DEFAULT_SDK_NAME = "logbrew-react-native";
19
- const DEFAULT_SDK_VERSION = "0.1.21";
19
+ const DEFAULT_SDK_VERSION = "0.1.22";
20
20
  const DEFAULT_ENDPOINT = "https://api.logbrew.co/v1/events";
21
21
  const MAX_ACTION_NAME_LENGTH = 64;
22
22
  const MAX_PRODUCT_ANALYTICS_SURFACE_LENGTH = 256;
@@ -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.21"
6
+ private static let sdkVersion = "0.1.22"
7
7
 
8
8
  private let lock = NSLock()
9
9
  private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
@@ -149,8 +149,11 @@ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
149
149
  return failure("native_diagnostics_not_installed")
150
150
  }
151
151
  do {
152
- let context = try rawContext.map { try NativeCrashCorrelation.validated($0) }
153
- try capture.setCorrelationContext(context)
152
+ let snapshot = try rawContext.map { try NativeCrashCorrelation.validated($0) }
153
+ try capture.setDiagnosticContext(
154
+ context: snapshot?.correlationContext,
155
+ impact: snapshot?.impact,
156
+ )
154
157
  return ["status": rawContext == nil ? "cleared" : "updated"]
155
158
  } catch let error as NativeCrashError {
156
159
  return failure(error.code.rawValue)
@@ -77,7 +77,8 @@ public struct IssueDiagnosticEvidence: Codable, Equatable, Sendable {
77
77
  }
78
78
  }
79
79
 
80
- func validateIssueDiagnosticEvidence(_ value: IssueDiagnosticEvidence) throws -> IssueDiagnosticEvidence {
80
+ @_spi(CrashReplay)
81
+ public func validateIssueDiagnosticEvidence(_ value: IssueDiagnosticEvidence) throws -> IssueDiagnosticEvidence {
81
82
  let cause = try value.likelyRootCause.map {
82
83
  try issueText($0, label: "issue evidence likelyRootCause", maximum: 1024)
83
84
  }
@@ -23,6 +23,9 @@ struct CrashReportSanitizer {
23
23
  let nativeStackFrames = NativeStackFrameSanitizer().frames(from: rawReport)
24
24
  let artifactIdentity = try NativeArtifactIdentityValue.persistedIdentity(in: rawReport)
25
25
  let correlation = NativeCrashCorrelation.captured(in: rawReport)
26
+ let correlationState = correlation.0?.correlationContext == nil && correlation.1 == .captured
27
+ ? NativeCrashContextState.notCaptured
28
+ : correlation.1
26
29
  let breadcrumbs = NativeCrashBreadcrumbs.captured(in: rawReport)
27
30
  return NativeCrashRecord(
28
31
  eventID: uuid.uuidString.lowercased(),
@@ -33,9 +36,10 @@ struct CrashReportSanitizer {
33
36
  context: nativeCrashContext(
34
37
  system: rawReport["system"] as? [String: Any],
35
38
  artifactIdentity: artifactIdentity,
36
- correlation: correlation.0,
39
+ correlation: correlation.0?.correlationContext,
37
40
  ),
38
- correlationState: correlation.1,
41
+ correlationState: correlationState,
42
+ impact: correlation.0?.impact,
39
43
  breadcrumbs: breadcrumbs.0?.breadcrumbs,
40
44
  breadcrumbsTruncated: breadcrumbs.0?.truncated,
41
45
  breadcrumbState: breadcrumbs.1,
@@ -109,17 +109,17 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
109
109
  /// Replaces the crash-time trace, session, and opaque subject snapshot in one bounded write.
110
110
  @nonobjc
111
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)
112
+ try setDiagnosticContext(context: context, impact: nil)
113
+ }
114
+
115
+ @nonobjc
116
+ func setDiagnosticContext(
117
+ context: TelemetryContext?,
118
+ impact: IssueImpactEvidence?,
119
+ ) throws {
120
+ try setSnapshot(forKey: NativeCrashCorrelation.reportKey) {
121
+ try NativeCrashCorrelation.encoded(context: context, impact: impact)
117
122
  }
118
- try verifyStorageLocked()
119
- try driver.setUserInfo(
120
- NativeCrashCorrelation.encoded(context),
121
- forKey: NativeCrashCorrelation.reportKey,
122
- )
123
123
  }
124
124
 
125
125
  /// Replaces the crash-time breadcrumb snapshot in one bounded write.
@@ -127,6 +127,15 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
127
127
  public func setBreadcrumbs(
128
128
  _ breadcrumbs: [IssueBreadcrumb]?,
129
129
  truncated: Bool = false,
130
+ ) throws {
131
+ try setSnapshot(forKey: NativeCrashBreadcrumbs.reportKey) {
132
+ try NativeCrashBreadcrumbs.encoded(breadcrumbs, truncated: truncated)
133
+ }
134
+ }
135
+
136
+ private func setSnapshot(
137
+ forKey key: String,
138
+ encoded: () throws -> String?,
130
139
  ) throws {
131
140
  lock.lock()
132
141
  defer { lock.unlock() }
@@ -135,10 +144,7 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
135
144
  throw NativeCrashError(.notInstalled)
136
145
  }
137
146
  try verifyStorageLocked()
138
- try driver.setUserInfo(
139
- NativeCrashBreadcrumbs.encoded(breadcrumbs, truncated: truncated),
140
- forKey: NativeCrashBreadcrumbs.reportKey,
141
- )
147
+ try driver.setUserInfo(encoded(), forKey: key)
142
148
  }
143
149
 
144
150
  @objc(replayPendingReportsWithHandler:error:)
@@ -8,16 +8,60 @@ enum NativeCrashContextState: String {
8
8
  case unavailable
9
9
  }
10
10
 
11
+ struct NativeCrashDiagnosticSnapshot: Codable, Equatable {
12
+ let schemaVersion: Int
13
+ let trace: TelemetryTraceContext?
14
+ let session: TelemetrySessionContext?
15
+ let subject: TelemetrySubjectContext?
16
+ let impact: IssueImpactEvidence?
17
+
18
+ init(context: TelemetryContext?, impact: IssueImpactEvidence?) throws {
19
+ let context = try context.map(validateNativeCrashCorrelationContext)
20
+ let impact = try impact.map { value in
21
+ guard value.affectedUserSegment == nil,
22
+ let value = try validateIssueDiagnosticEvidence(
23
+ IssueDiagnosticEvidence(impact: value),
24
+ ).impact,
25
+ value.failedAction != nil
26
+ else {
27
+ throw NativeCrashError(.invalidConfiguration)
28
+ }
29
+ return value
30
+ }
31
+ guard context != nil || impact != nil else {
32
+ throw NativeCrashError(.invalidConfiguration)
33
+ }
34
+ schemaVersion = 1
35
+ trace = context?.trace
36
+ session = context?.session
37
+ subject = context?.subject
38
+ self.impact = impact
39
+ }
40
+
41
+ var correlationContext: TelemetryContext? {
42
+ trace == nil && session == nil && subject == nil
43
+ ? nil
44
+ : TelemetryContext(trace: trace, session: session, subject: subject)
45
+ }
46
+
47
+ }
48
+
11
49
  enum NativeCrashCorrelation {
12
50
  static let reportKey = "logbrew_native_correlation"
13
- private static let maximumBytes = 1024
51
+ private static let maximumBytes = 4 * 1024
14
52
 
15
- static func encoded(_ context: TelemetryContext?) throws -> String? {
16
- guard let context else {
53
+ static func encoded(
54
+ context: TelemetryContext?,
55
+ impact: IssueImpactEvidence? = nil,
56
+ ) throws -> String? {
57
+ guard context != nil || impact != nil else {
17
58
  return nil
18
59
  }
19
60
  do {
20
- let data = try encoder().encode(validateNativeCrashCorrelationContext(context))
61
+ let data = try nativeCrashEncoded(NativeCrashDiagnosticSnapshot(
62
+ context: context,
63
+ impact: impact,
64
+ ))
21
65
  guard data.count <= maximumBytes, let value = String(data: data, encoding: .utf8) else {
22
66
  throw NativeCrashError(.invalidConfiguration)
23
67
  }
@@ -27,51 +71,23 @@ enum NativeCrashCorrelation {
27
71
  }
28
72
  }
29
73
 
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)
74
+ static func captured(
75
+ in rawReport: [String: Any],
76
+ ) -> (NativeCrashDiagnosticSnapshot?, NativeCrashContextState) {
77
+ nativeCrashCaptured(in: rawReport, key: reportKey, maximumBytes: maximumBytes, validate: validated)
43
78
  }
44
79
 
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 {
80
+ static func validated(_ object: Any) throws -> NativeCrashDiagnosticSnapshot {
81
+ try nativeCrashValidated(object, maximumBytes: maximumBytes) { decoded in
82
+ guard decoded.schemaVersion == 1 else {
52
83
  throw NativeCrashError(.invalidConfiguration)
53
84
  }
54
- let context = try validateNativeCrashCorrelationContext(
55
- JSONDecoder().decode(TelemetryContext.self, from: data),
85
+ return try NativeCrashDiagnosticSnapshot(
86
+ context: decoded.correlationContext,
87
+ impact: decoded.impact,
56
88
  )
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
89
  }
68
90
  }
69
-
70
- private static func encoder() -> JSONEncoder {
71
- let encoder = JSONEncoder()
72
- encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
73
- return encoder
74
- }
75
91
  }
76
92
 
77
93
  struct NativeCrashBreadcrumbSnapshot: Codable, Equatable {
@@ -98,7 +114,7 @@ enum NativeCrashBreadcrumbs {
98
114
  var breadcrumbs = try values.suffix(maximumIssueBreadcrumbs).map(validateIssueBreadcrumb)
99
115
  var wasTruncated = truncated || breadcrumbs.count < values.count
100
116
  while !breadcrumbs.isEmpty {
101
- let data = try encoder().encode(NativeCrashBreadcrumbSnapshot(
117
+ let data = try nativeCrashEncoded(NativeCrashBreadcrumbSnapshot(
102
118
  breadcrumbs: breadcrumbs,
103
119
  truncated: wasTruncated,
104
120
  ))
@@ -119,56 +135,74 @@ enum NativeCrashBreadcrumbs {
119
135
  static func captured(
120
136
  in rawReport: [String: Any],
121
137
  ) -> (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)
138
+ nativeCrashCaptured(in: rawReport, key: reportKey, maximumBytes: maximumBytes, validate: validated)
134
139
  }
135
140
 
136
141
  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)
142
+ try nativeCrashValidated(object, maximumBytes: maximumBytes) { decoded in
146
143
  guard decoded.schemaVersion == 1,
147
144
  !decoded.breadcrumbs.isEmpty,
148
145
  decoded.breadcrumbs.count <= maximumIssueBreadcrumbs
149
146
  else {
150
147
  throw NativeCrashError(.invalidConfiguration)
151
148
  }
152
- let snapshot = try NativeCrashBreadcrumbSnapshot(
149
+ return try NativeCrashBreadcrumbSnapshot(
153
150
  breadcrumbs: decoded.breadcrumbs.map(validateIssueBreadcrumb),
154
151
  truncated: decoded.truncated,
155
152
  )
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
153
  }
167
154
  }
155
+ }
168
156
 
169
- private static func encoder() -> JSONEncoder {
170
- let encoder = JSONEncoder()
171
- encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
172
- return encoder
157
+ private func nativeCrashCaptured<Value>(
158
+ in rawReport: [String: Any],
159
+ key: String,
160
+ maximumBytes: Int,
161
+ validate: (Any) throws -> Value,
162
+ ) -> (Value?, NativeCrashContextState) {
163
+ guard let user = rawReport["user"] as? [String: Any], let rawValue = user[key] else {
164
+ return (nil, .notCaptured)
165
+ }
166
+ guard let value = rawValue as? String,
167
+ let data = value.data(using: .utf8),
168
+ data.count <= maximumBytes,
169
+ let object = try? JSONSerialization.jsonObject(with: data),
170
+ let snapshot = try? validate(object)
171
+ else {
172
+ return (nil, .unavailable)
173
+ }
174
+ return (snapshot, .captured)
175
+ }
176
+
177
+ private func nativeCrashValidated<Value: Codable>(
178
+ _ object: Any,
179
+ maximumBytes: Int,
180
+ normalize: (Value) throws -> Value,
181
+ ) throws -> Value {
182
+ do {
183
+ guard JSONSerialization.isValidJSONObject(object) else {
184
+ throw NativeCrashError(.invalidConfiguration)
185
+ }
186
+ let source = try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
187
+ guard source.count <= maximumBytes else {
188
+ throw NativeCrashError(.invalidConfiguration)
189
+ }
190
+ let value = try normalize(JSONDecoder().decode(Value.self, from: source))
191
+ let normalized = try nativeCrashEncoded(value)
192
+ let normalizedObject = try JSONSerialization.jsonObject(with: normalized)
193
+ guard object as? NSDictionary == normalizedObject as? NSDictionary else {
194
+ throw NativeCrashError(.invalidConfiguration)
195
+ }
196
+ return value
197
+ } catch let error as NativeCrashError {
198
+ throw error
199
+ } catch {
200
+ throw NativeCrashError(.invalidConfiguration)
173
201
  }
174
202
  }
203
+
204
+ private func nativeCrashEncoded(_ value: some Encodable) throws -> Data {
205
+ let encoder = JSONEncoder()
206
+ encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
207
+ return try encoder.encode(value)
208
+ }
@@ -165,6 +165,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
165
165
  private let artifactIdentity: NativeArtifactIdentity?
166
166
  private let context: TelemetryContext?
167
167
  private let correlationState: NativeCrashContextState?
168
+ private let impact: IssueImpactEvidence?
168
169
  private let breadcrumbs: [IssueBreadcrumb]?
169
170
  private let breadcrumbsTruncated: Bool?
170
171
  private let breadcrumbState: NativeCrashContextState?
@@ -182,6 +183,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
182
183
  artifactIdentity: NativeArtifactIdentity?,
183
184
  context: TelemetryContext?,
184
185
  correlationState: NativeCrashContextState? = nil,
186
+ impact: IssueImpactEvidence? = nil,
185
187
  breadcrumbs: [IssueBreadcrumb]? = nil,
186
188
  breadcrumbsTruncated: Bool? = nil,
187
189
  breadcrumbState: NativeCrashContextState? = nil,
@@ -198,6 +200,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
198
200
  self.artifactIdentity = artifactIdentity
199
201
  self.context = context
200
202
  self.correlationState = correlationState
203
+ self.impact = impact
201
204
  self.breadcrumbs = breadcrumbs
202
205
  self.breadcrumbsTruncated = breadcrumbsTruncated
203
206
  self.breadcrumbState = breadcrumbState
@@ -259,6 +262,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
259
262
  exceptionChain: nativeCrashExceptionChain(for: issueException),
260
263
  breadcrumbs: breadcrumbs,
261
264
  breadcrumbsTruncated: breadcrumbsTruncated,
265
+ evidence: impact.map { IssueDiagnosticEvidence(impact: $0) },
262
266
  metadata: metadata,
263
267
  context: context,
264
268
  nativeStackFrames: nativeStackFrames,
@@ -301,7 +305,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
301
305
  else {
302
306
  return false
303
307
  }
304
- let legacyFields = ["exception", "exceptionChain", "breadcrumbs", "breadcrumbsTruncated", "context"]
308
+ let legacyFields = ["exception", "exceptionChain", "evidence", "breadcrumbs", "breadcrumbsTruncated", "context"]
305
309
  for legacyField in legacyFields where actual[legacyField] == nil {
306
310
  expected.removeValue(forKey: legacyField)
307
311
  }
@@ -10,7 +10,7 @@
10
10
  "swift/logbrew-swift/Sources/LogBrew/DurableDeliveryStore.swift": "7c292dda68efc3de790daf44733b25e4392226793343c21ba5df4f51fc1b402b",
11
11
  "swift/logbrew-swift/Sources/LogBrew/DurableDeliveryStoreRecovery.swift": "79fee27975b6571954bf0b3692a50e60bbbcf53b5ab47f653a474db5c56d68ba",
12
12
  "swift/logbrew-swift/Sources/LogBrew/EventEncoding.swift": "b88b6b2d17f7d0cee9ac1a1f34ef35b192cd33d213ef3c9e1c2d21cbcc304a97",
13
- "swift/logbrew-swift/Sources/LogBrew/IssueDiagnosticEvidence.swift": "e471f27fadaefe06971c1a98892aeb0774e840a2ce0cbb243ae9aabfa41b1c47",
13
+ "swift/logbrew-swift/Sources/LogBrew/IssueDiagnosticEvidence.swift": "1042b5a6283cf440529572844e5ad1f3078f4ba715133932ea305e075614a53c",
14
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",
@@ -29,13 +29,13 @@
29
29
  "swift/logbrew-swift/Sources/LogBrew/URLSessionTracer.swift": "0cdf08f92d6111c0dc2883b87837b81f69b5c2f9a8e5501780a114c23a741af7",
30
30
  "swift/logbrew-swift/Sources/LogBrew/Validation.swift": "07ff5c2daeb829b1df4f50edb0b79727f9a81b1c9b5cde5f995f5ad6ce660aa7",
31
31
  "swift/logbrew-swift/Sources/LogBrewCrash/CrashEngine.swift": "a94dfce653e3e7f381be42b6d2c4540f0be77e83777ecc53e06682cd2e569c5a",
32
- "swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "ccd0ef83f14096816765278e007f5e4ba0a40286ce00d708b38ca0b94959171e",
32
+ "swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "a56a2f6c6c7f1db900804a37df51aebba7b83dce14fa7816de5753c33409e4a5",
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": "c2f5d0e9a8953cb988633ebf60a06f3b7ddaa0c9828f99372969fdfb60b99bc4",
36
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "eef19c6e561bc03bb4c09736722afe57f63d557f9279b26353679bd53362c25d",
35
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCapture.swift": "0b773a63987b6c694b044e56c0f81878f4f5e71eae3fdd126744cba3cf15aac1",
36
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "e5a7c25448c83ca2ae149c650ecfc72c5193741291a6ff9623eabadad8a9c440",
37
37
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashExceptionEvidence.swift": "ce191e46298d91d87b379528d610caa88b924b15a8dd912b4a164b51eb172a7a",
38
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "2d0f2e7cdce09dc09906cede5d83928e8bd0e70a8522ff55c999d19366d84730",
38
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "70aa9700efd26d03b9656f1df96876de0b9eb769c03512ec53f667696773954f",
39
39
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashReplay.swift": "3eb3d93b32cf14de416f56fa30d6d65e6f268b5ec9d1a67081d7525c4efd4f80",
40
40
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangIncidentStore.swift": "5a8150083f16d6b495c9c63434809dc5f11f13ebb23211f57151bad62040781b",
41
41
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdog.swift": "37b25904cd6576e20c40500be2c153156403b57ad7b4186a1b6b35b414145ecb",
package/lifecycle.js CHANGED
@@ -1,121 +1,7 @@
1
- import { SdkError } from "@logbrew/sdk";
2
- import {
3
- createReactNativeSpanAttributes,
4
- createReactNativeTraceContext,
5
- getActiveLogBrewTrace,
6
- getReactNativeContext
7
- } from "./index.js";
1
+ import runtime from "./lifecycle.cjs";
8
2
 
9
- export function createReactNativeLifecycleSpanEvent({
10
- durationMs, fromState, id, idFactory = defaultLifecycleSpanEventId, metadata = {}, name,
11
- now = () => new Date().toISOString(), platform, appState, screen, sessionId, state, status = "ok",
12
- timestamp, toState, trace
13
- } = {}) {
14
- const safeFromState = normalizeLifecycleState(fromState);
15
- const safeToState = normalizeLifecycleState(toState ?? state);
16
- const transition = [safeFromState, safeToState].filter(Boolean).join("->");
17
- const spanName = name ?? `app_state:${transition || safeToState || safeFromState || "change"}`;
18
- const activeTrace = trace ?? getActiveLogBrewTrace() ?? createReactNativeTraceContext();
19
- return {
20
- id: id ?? idFactory({ fromState: safeFromState, screen, toState: safeToState }),
21
- timestamp: timestamp ?? now(),
22
- attributes: createReactNativeSpanAttributes({
23
- name: spanName,
24
- status,
25
- durationMs,
26
- trace: activeTrace,
27
- metadata: {
28
- ...getReactNativeContext({ platform, appState }),
29
- source: "react-native.lifecycle",
30
- appState: safeToState,
31
- durationMs,
32
- fromAppState: safeFromState,
33
- screen,
34
- sessionId,
35
- toAppState: safeToState,
36
- ...metadata
37
- }
38
- })
39
- };
40
- }
41
-
42
- export function captureReactNativeLifecycleSpan(client, input = {}) {
43
- requireClient(client);
44
- const event = createReactNativeLifecycleSpanEvent(input);
45
- client.span(event.id, event.timestamp, event.attributes);
46
- return event;
47
- }
48
-
49
- export function createAppStateLifecycleSpanListener(client, appState, {
50
- captureInitialState = false, metadata = {}, now = () => new Date().toISOString(), nowMs = () => Date.now(),
51
- onError, platform, screen, sessionId, trace
52
- } = {}) {
53
- requireClient(client);
54
- if (!appState || typeof appState.addEventListener !== "function") {
55
- throw new SdkError("configuration_error", "createAppStateLifecycleSpanListener requires AppState.addEventListener");
56
- }
57
-
58
- let previousState = normalizeLifecycleState(appState.currentState);
59
- let previousChangedAtMs = nowMs();
60
- if (captureInitialState && previousState !== undefined) {
61
- captureReactNativeLifecycleSpan(client, {
62
- appState, metadata, now, platform, screen, sessionId, toState: previousState, trace
63
- });
64
- }
65
-
66
- const subscription = appState.addEventListener("change", (nextState) => {
67
- try {
68
- const safeNextState = normalizeLifecycleState(nextState);
69
- if (safeNextState === undefined) {
70
- return;
71
- }
72
- const changedAtMs = nowMs();
73
- const durationMs = previousState === undefined ? undefined : Math.max(0, changedAtMs - previousChangedAtMs);
74
- captureReactNativeLifecycleSpan(client, {
75
- appState, durationMs, fromState: previousState, metadata, now, platform, screen, sessionId,
76
- timestamp: now(), toState: safeNextState, trace
77
- });
78
- previousState = safeNextState;
79
- previousChangedAtMs = changedAtMs;
80
- } catch (error) {
81
- if (typeof onError === "function") {
82
- onError(error);
83
- } else {
84
- throw error;
85
- }
86
- }
87
- });
88
-
89
- return subscriptionRemover(subscription);
90
- }
91
-
92
- function requireClient(client) {
93
- if (!client) {
94
- throw new SdkError("configuration_error", "LogBrew React Native lifecycle helpers require a client");
95
- }
96
- }
97
-
98
- function defaultLifecycleSpanEventId({ fromState, screen, toState }) {
99
- return `evt_native_lifecycle_${slugify([screen, fromState, toState].filter(Boolean).join("_") || "app_state")}`;
100
- }
101
-
102
- function normalizeLifecycleState(state) {
103
- return typeof state === "string" && state.trim() !== "" ? state.trim() : undefined;
104
- }
105
-
106
- function subscriptionRemover(subscription) {
107
- if (typeof subscription === "function") {
108
- return subscription;
109
- }
110
- if (subscription && typeof subscription.remove === "function") {
111
- return () => subscription.remove();
112
- }
113
- return () => {};
114
- }
115
-
116
- function slugify(value) {
117
- return String(value)
118
- .toLowerCase()
119
- .replace(/[^a-z0-9]+/g, "_")
120
- .replace(/^_+|_+$/g, "") || "event";
121
- }
3
+ export const {
4
+ captureReactNativeLifecycleSpan,
5
+ createAppStateLifecycleSpanListener,
6
+ createReactNativeLifecycleSpanEvent
7
+ } = runtime;
package/native-bridge.js CHANGED
@@ -1,125 +1,10 @@
1
- import { SdkError } from "@logbrew/sdk";
2
- import {
3
- getActiveLogBrewTrace,
4
- getReactNativeTraceMetadata
5
- } from "./index.js";
1
+ import runtime from "./native-bridge.cjs";
6
2
 
7
- const DEFAULT_SCOPE_SOURCE = "react-native.native_bridge";
8
- const RESERVED_TRACE_METADATA_KEYS = new Set([
9
- "parentSpanId",
10
- "spanId",
11
- "traceFlags",
12
- "traceId",
13
- "traceSampled",
14
- "traceparent"
15
- ]);
16
-
17
- export function createLogBrewNativeBridgeScope({
18
- logger,
19
- metadata = {},
20
- screen,
21
- sessionId,
22
- source = DEFAULT_SCOPE_SOURCE,
23
- trace = getActiveLogBrewTrace()
24
- } = {}) {
25
- const traceMetadata = getReactNativeTraceMetadata(trace);
26
- return {
27
- trace: Object.keys(traceMetadata).length === 0 ? undefined : {
28
- parentSpanId: traceMetadata.parentSpanId,
29
- spanId: traceMetadata.spanId,
30
- traceFlags: traceMetadata.traceFlags,
31
- traceId: traceMetadata.traceId,
32
- traceSampled: traceMetadata.traceSampled
33
- },
34
- metadata: compactMetadata({
35
- ...metadata,
36
- logger,
37
- screen,
38
- sessionId,
39
- source
40
- })
41
- };
42
- }
43
-
44
- export function syncLogBrewNativeBridgeScope(nativeBridge, options = {}) {
45
- const payload = createLogBrewNativeBridgeScope(options);
46
- bridgeSync(nativeBridge)(payload);
47
- return payload;
48
- }
49
-
50
- export function clearLogBrewNativeBridgeScope(nativeBridge) {
51
- bridgeClear(nativeBridge)();
52
- }
53
-
54
- export function withLogBrewNativeBridgeScope(nativeBridge, options, callback) {
55
- const resolvedOptions = typeof options === "function" ? {} : options ?? {};
56
- const resolvedCallback = typeof options === "function" ? options : callback;
57
- if (typeof resolvedCallback !== "function") {
58
- throw new SdkError("configuration_error", "withLogBrewNativeBridgeScope requires a callback");
59
- }
60
-
61
- const payload = syncLogBrewNativeBridgeScope(nativeBridge, resolvedOptions);
62
- try {
63
- const result = resolvedCallback(payload);
64
- if (result && typeof result.then === "function") {
65
- return Promise.resolve(result).finally(() => clearLogBrewNativeBridgeScope(nativeBridge));
66
- }
67
- clearLogBrewNativeBridgeScope(nativeBridge);
68
- return result;
69
- } catch (error) {
70
- clearLogBrewNativeBridgeScope(nativeBridge);
71
- throw error;
72
- }
73
- }
74
-
75
- function bridgeSync(nativeBridge) {
76
- if (typeof nativeBridge === "function") {
77
- return nativeBridge;
78
- }
79
- if (nativeBridge && typeof nativeBridge.setLogBrewScope === "function") {
80
- return nativeBridge.setLogBrewScope.bind(nativeBridge);
81
- }
82
- if (nativeBridge && typeof nativeBridge.syncLogBrewScope === "function") {
83
- return nativeBridge.syncLogBrewScope.bind(nativeBridge);
84
- }
85
- throw new SdkError(
86
- "configuration_error",
87
- "LogBrew native bridge scope sync requires a function, setLogBrewScope, or syncLogBrewScope"
88
- );
89
- }
90
-
91
- function bridgeClear(nativeBridge) {
92
- if (nativeBridge && typeof nativeBridge.clearLogBrewScope === "function") {
93
- return nativeBridge.clearLogBrewScope.bind(nativeBridge);
94
- }
95
- if (nativeBridge && typeof nativeBridge.clearLogBrewTraceContext === "function") {
96
- return nativeBridge.clearLogBrewTraceContext.bind(nativeBridge);
97
- }
98
- if (typeof nativeBridge === "function") {
99
- return () => nativeBridge(undefined);
100
- }
101
- return () => {};
102
- }
103
-
104
- function compactMetadata(metadata) {
105
- const compacted = {};
106
- for (const [key, value] of Object.entries(metadata)) {
107
- if (value === undefined) {
108
- continue;
109
- }
110
- if (RESERVED_TRACE_METADATA_KEYS.has(key)) {
111
- continue;
112
- }
113
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
114
- compacted[key] = value;
115
- }
116
- }
117
- return compacted;
118
- }
119
-
120
- export default {
3
+ export const {
121
4
  clearLogBrewNativeBridgeScope,
122
5
  createLogBrewNativeBridgeScope,
123
6
  syncLogBrewNativeBridgeScope,
124
7
  withLogBrewNativeBridgeScope
125
- };
8
+ } = runtime;
9
+
10
+ export default runtime.default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/react-native",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
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",