@logbrew/react-native 0.1.16 → 0.1.18

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.
@@ -33,7 +33,7 @@ Pod::Spec.new do |spec|
33
33
  "ios/GeneratedAppleDiagnostics/**/*.swift"
34
34
  ]
35
35
  diagnostics.dependency "#{spec.name}/Core"
36
- diagnostics.dependency "KSCrash/Recording", "2.5.1"
36
+ diagnostics.dependency "KSCrash/Recording", "2.6.0"
37
37
  diagnostics.pod_target_xcconfig = {
38
38
  "DEFINES_MODULE" => "YES",
39
39
  "SWIFT_VERSION" => "5.0"
package/README.md CHANGED
@@ -335,7 +335,8 @@ the configured project does not replay an earlier project's records with the
335
335
  new project's key. The raw local KSCrash report can still contain sensitive
336
336
  process details, so apply the app's consent and retention policy to it. Hosted
337
337
  replay uploads only the bounded crash or hang issue described by the Swift
338
- `LogBrewCrash` contract. Status and error results expose fixed codes and bounded
338
+ `LogBrewCrash` contract, including its capture-time service, deployment, OS,
339
+ device-class, and application-version context. Status and error results expose fixed codes and bounded
339
340
  counters, not keys, paths, raw reports, or native error text. A custom endpoint
340
341
  must be a plain HTTPS path without embedded authentication, a query, or a fragment.
341
342
 
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.16";
19
+ const DEFAULT_SDK_VERSION = "0.1.18";
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;
@@ -595,6 +595,7 @@ function createReactNavigationSpanListener(client, navigationContainer, {
595
595
 
596
596
  function createReactNativeErrorEvent(error, {
597
597
  debugIdMap,
598
+ evidence,
598
599
  environment,
599
600
  fingerprint,
600
601
  handled = true,
@@ -618,6 +619,7 @@ function createReactNativeErrorEvent(error, {
618
619
  const traceContext = resolveTraceContext(trace ?? getActiveLogBrewTrace());
619
620
  const attributes = createIssueAttributesFromError(error?.reason ?? error?.error ?? error, {
620
621
  debugIdMap: debugIdMap === undefined ? runtimeReactNativeDebugIdMap() : debugIdMap,
622
+ evidence,
621
623
  environment,
622
624
  fingerprint,
623
625
  handled,
package/index.d.cts CHANGED
@@ -20,9 +20,7 @@ export type ReactNativePlatformLike = {
20
20
  OS?: string;
21
21
  Version?: string | number;
22
22
  isPad?: boolean;
23
- constants?: {
24
- isTesting?: boolean;
25
- };
23
+ constants?: { isTesting?: boolean; [key: string]: unknown };
26
24
  };
27
25
 
28
26
  export type ReactNativeAppStateLike = {
@@ -287,6 +285,7 @@ export type ReactNativeErrorIdFactoryContext = {
287
285
 
288
286
  export type CaptureReactNativeErrorOptions = ReactNativeContextOptions & {
289
287
  debugIdMap?: Record<string, string>;
288
+ evidence?: IssueAttributes["evidence"];
290
289
  environment?: string;
291
290
  fingerprint?: string;
292
291
  handled?: boolean;
package/index.d.ts CHANGED
@@ -20,9 +20,7 @@ export type ReactNativePlatformLike = {
20
20
  OS?: string;
21
21
  Version?: string | number;
22
22
  isPad?: boolean;
23
- constants?: {
24
- isTesting?: boolean;
25
- };
23
+ constants?: { isTesting?: boolean; [key: string]: unknown };
26
24
  };
27
25
 
28
26
  export type ReactNativeAppStateLike = {
@@ -287,6 +285,7 @@ export type ReactNativeErrorIdFactoryContext = {
287
285
 
288
286
  export type CaptureReactNativeErrorOptions = ReactNativeContextOptions & {
289
287
  debugIdMap?: Record<string, string>;
288
+ evidence?: IssueAttributes["evidence"];
290
289
  environment?: string;
291
290
  fingerprint?: string;
292
291
  handled?: boolean;
package/index.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "./metadata.js";
17
17
 
18
18
  const DEFAULT_SDK_NAME = "logbrew-react-native";
19
- const DEFAULT_SDK_VERSION = "0.1.16";
19
+ const DEFAULT_SDK_VERSION = "0.1.18";
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;
@@ -584,6 +584,7 @@ export function createReactNavigationSpanListener(client, navigationContainer, {
584
584
 
585
585
  export function createReactNativeErrorEvent(error, {
586
586
  debugIdMap,
587
+ evidence,
587
588
  environment,
588
589
  fingerprint,
589
590
  handled = true,
@@ -607,6 +608,7 @@ export function createReactNativeErrorEvent(error, {
607
608
  const traceContext = resolveTraceContext(trace ?? getActiveLogBrewTrace());
608
609
  const attributes = createIssueAttributesFromError(error?.reason ?? error?.error ?? error, {
609
610
  debugIdMap: debugIdMap === undefined ? runtimeReactNativeDebugIdMap() : debugIdMap,
611
+ evidence,
610
612
  environment,
611
613
  fingerprint,
612
614
  handled,
@@ -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.16"
6
+ private static let sdkVersion = "0.1.18"
7
7
 
8
8
  private let lock = NSLock()
9
9
  private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
@@ -76,7 +76,7 @@ final class DurableDeliveryStore: @unchecked Sendable {
76
76
  recoveredEvents = recovery.events
77
77
  recoveredPrefix = recovery.prefix
78
78
  if let last = recovery.events.last {
79
- let lastSequence = try Self.sequence(from: last)
79
+ let lastSequence = try Self.sequence(from: last.recordName)
80
80
  guard lastSequence < UInt64.max else {
81
81
  throw DurableStoreFailure.corrupt
82
82
  }
@@ -109,7 +109,7 @@ final class DurableDeliveryStore: @unchecked Sendable {
109
109
  throw DurableStoreFailure.corrupt
110
110
  }
111
111
  let name = Self.eventName(sequence: nextSequence)
112
- let eventData = try Self.encodeEvent(event)
112
+ let eventData = try Self.encode(event)
113
113
  guard eventData.count == encodedBytes else {
114
114
  throw DurableStoreFailure.corrupt
115
115
  }
@@ -1,6 +1,14 @@
1
1
  // Generated by scripts/sync-apple-native-sources.mjs.
2
2
  // Edit the canonical Swift source, then regenerate this package boundary.
3
- import Darwin
3
+ #if canImport(Musl)
4
+ import Musl
5
+ #elseif canImport(Glibc)
6
+ import Glibc
7
+ #elseif canImport(Darwin)
8
+ import Darwin
9
+ #else
10
+ #error("LogBrew durable delivery requires a POSIX platform")
11
+ #endif
4
12
  import Foundation
5
13
 
6
14
  extension DurableDeliveryStore {
@@ -167,13 +175,8 @@ extension DurableDeliveryStore {
167
175
  String(format: "event-%020llu.json", sequence)
168
176
  }
169
177
 
170
- static func sequence(from recovered: RecoveredEvent) throws -> UInt64 {
171
- try sequence(from: recovered.recordName)
172
- }
173
-
174
178
  static func sequence(from name: String) throws -> UInt64 {
175
- let prefix = "event-"
176
- let suffix = ".json"
179
+ let (prefix, suffix) = ("event-", ".json")
177
180
  guard name.hasPrefix(prefix), name.hasSuffix(suffix) else {
178
181
  throw DurableStoreFailure.corrupt
179
182
  }
@@ -204,10 +207,6 @@ extension DurableDeliveryStore {
204
207
  }
205
208
  }
206
209
 
207
- static func encodeEvent(_ event: Event) throws -> Data {
208
- try encode(event)
209
- }
210
-
211
210
  static func checksum(_ data: Data) -> String {
212
211
  var hash: UInt64 = 14_695_981_039_346_656_037
213
212
  for byte in data {
@@ -259,7 +258,7 @@ extension DurableDeliveryStore {
259
258
  throw DurableStoreFailure.corrupt
260
259
  }
261
260
  let record = try decodeEventRecord(from: file)
262
- let encodedEvent = try encodeEvent(record.event)
261
+ let encodedEvent = try encode(record.event)
263
262
  guard record.version == version,
264
263
  record.sequence == sequence,
265
264
  record.sdk == sdk,
@@ -0,0 +1,184 @@
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
+ /// App-reported code location that narrows the smallest likely fix area.
6
+ public struct IssueLikelyFixArea: Codable, Equatable, Sendable {
7
+ public let component: String?
8
+ public let module: String?
9
+ public let function: String?
10
+ public let file: String?
11
+ public let line: Int?
12
+ public let column: Int?
13
+ public let inApp: Bool?
14
+
15
+ public init(
16
+ component: String? = nil,
17
+ module: String? = nil,
18
+ function: String? = nil,
19
+ file: String? = nil,
20
+ line: Int? = nil,
21
+ column: Int? = nil,
22
+ inApp: Bool? = nil,
23
+ ) {
24
+ self.component = component
25
+ self.module = module
26
+ self.function = function
27
+ self.file = file
28
+ self.line = line
29
+ self.column = column
30
+ self.inApp = inApp
31
+ }
32
+ }
33
+
34
+ /// App-reported impact without user identities or raw request data.
35
+ public struct IssueImpactEvidence: Codable, Equatable, Sendable {
36
+ public let affectedUserSegment: String?
37
+ public let failedAction: String?
38
+ public let userVisibleOutcome: String?
39
+
40
+ public init(
41
+ affectedUserSegment: String? = nil,
42
+ failedAction: String? = nil,
43
+ userVisibleOutcome: String? = nil,
44
+ ) {
45
+ self.affectedUserSegment = affectedUserSegment
46
+ self.failedAction = failedAction
47
+ self.userVisibleOutcome = userVisibleOutcome
48
+ }
49
+ }
50
+
51
+ /// Explicit app-reported cause, fix, impact, and capture-state evidence.
52
+ public struct IssueDiagnosticEvidence: Codable, Equatable, Sendable {
53
+ public let likelyRootCause: String?
54
+ public let likelyFixArea: IssueLikelyFixArea?
55
+ public let impact: IssueImpactEvidence?
56
+ public let capturedFields: [String]?
57
+ public let missingFields: [String]?
58
+ public let redactedFields: [String]?
59
+ public let truncatedFields: [String]?
60
+
61
+ public init(
62
+ likelyRootCause: String? = nil,
63
+ likelyFixArea: IssueLikelyFixArea? = nil,
64
+ impact: IssueImpactEvidence? = nil,
65
+ capturedFields: [String]? = nil,
66
+ missingFields: [String]? = nil,
67
+ redactedFields: [String]? = nil,
68
+ truncatedFields: [String]? = nil,
69
+ ) {
70
+ self.likelyRootCause = likelyRootCause
71
+ self.likelyFixArea = likelyFixArea
72
+ self.impact = impact
73
+ self.capturedFields = capturedFields
74
+ self.missingFields = missingFields
75
+ self.redactedFields = redactedFields
76
+ self.truncatedFields = truncatedFields
77
+ }
78
+ }
79
+
80
+ func validateIssueDiagnosticEvidence(_ value: IssueDiagnosticEvidence) throws -> IssueDiagnosticEvidence {
81
+ let cause = try value.likelyRootCause.map {
82
+ try issueText($0, label: "issue evidence likelyRootCause", maximum: 1024)
83
+ }
84
+ let fixArea = try value.likelyFixArea.map(validateIssueLikelyFixArea)
85
+ let impact = try value.impact.map(validateIssueImpactEvidence)
86
+ var seen = Set<String>()
87
+ let fieldLists = try [
88
+ ("capturedFields", value.capturedFields),
89
+ ("missingFields", value.missingFields),
90
+ ("redactedFields", value.redactedFields),
91
+ ("truncatedFields", value.truncatedFields),
92
+ ].map { name, fields in
93
+ try fields.map { try validateIssueEvidenceFields($0, name: name, seen: &seen) }
94
+ }
95
+ guard cause != nil || fixArea != nil || impact != nil || fieldLists.contains(where: { $0 != nil }) else {
96
+ throw issueValidationError("issue evidence must contain at least one field")
97
+ }
98
+ return IssueDiagnosticEvidence(
99
+ likelyRootCause: cause,
100
+ likelyFixArea: fixArea,
101
+ impact: impact,
102
+ capturedFields: fieldLists[0],
103
+ missingFields: fieldLists[1],
104
+ redactedFields: fieldLists[2],
105
+ truncatedFields: fieldLists[3],
106
+ )
107
+ }
108
+
109
+ private func validateIssueLikelyFixArea(_ value: IssueLikelyFixArea) throws -> IssueLikelyFixArea {
110
+ let identities = try [value.component, value.module, value.function].map { identity in
111
+ try identity.map {
112
+ try issueText($0, label: "issue evidence likelyFixArea identity", maximum: 256,
113
+ disallowLocationDelimiters: true)
114
+ }
115
+ }
116
+ let file = try value.file.map(validateIssueRelativePath)
117
+ let coordinates = try [value.line, value.column].map { coordinate in
118
+ try coordinate.map {
119
+ guard (1 ... Int(Int32.max)).contains($0) else {
120
+ throw issueValidationError("issue evidence likelyFixArea coordinate must be a positive integer")
121
+ }
122
+ return $0
123
+ }
124
+ }
125
+ guard identities.contains(where: { $0 != nil }) || file != nil || coordinates.contains(where: { $0 != nil }) else {
126
+ throw issueValidationError("issue evidence likelyFixArea must identify a code location")
127
+ }
128
+ return IssueLikelyFixArea(
129
+ component: identities[0], module: identities[1], function: identities[2], file: file,
130
+ line: coordinates[0], column: coordinates[1], inApp: value.inApp,
131
+ )
132
+ }
133
+
134
+ private func validateIssueImpactEvidence(_ value: IssueImpactEvidence) throws -> IssueImpactEvidence {
135
+ let values = try [
136
+ (value.affectedUserSegment, 256, true),
137
+ (value.failedAction, 256, true),
138
+ (value.userVisibleOutcome, 512, false),
139
+ ].map { text, maximum, rejectLocation in
140
+ try text.map {
141
+ try issueText($0, label: "issue evidence impact", maximum: maximum,
142
+ disallowLocationDelimiters: rejectLocation)
143
+ }
144
+ }
145
+ guard values.contains(where: { $0 != nil }) else {
146
+ throw issueValidationError("issue evidence impact must contain at least one field")
147
+ }
148
+ return IssueImpactEvidence(
149
+ affectedUserSegment: values[0], failedAction: values[1], userVisibleOutcome: values[2],
150
+ )
151
+ }
152
+
153
+ private func validateIssueEvidenceFields(
154
+ _ fields: [String],
155
+ name: String,
156
+ seen: inout Set<String>,
157
+ ) throws -> [String] {
158
+ guard (1 ... 32).contains(fields.count), Set(fields).count == fields.count,
159
+ fields.allSatisfy({ validMachineKey($0, maximum: 64, separators: ["_", ".", "-"], allowNumericStart: true) })
160
+ else {
161
+ throw issueValidationError("issue evidence \(name) must contain 1-32 unique bounded fields")
162
+ }
163
+ for field in fields where !seen.insert(field).inserted {
164
+ throw issueValidationError("issue evidence field \(field) has conflicting states")
165
+ }
166
+ return fields
167
+ }
168
+
169
+ private func validateIssueRelativePath(_ value: String) throws -> String {
170
+ let path = try issueText(
171
+ value.replacingOccurrences(of: "\\", with: "/"),
172
+ label: "issue evidence likelyFixArea file",
173
+ maximum: 256,
174
+ disallowLocationDelimiters: true,
175
+ )
176
+ let parts = path.split(separator: "/", omittingEmptySubsequences: false)
177
+ let drivePath = path.count >= 3 && path[path.index(after: path.startIndex)] == ":"
178
+ guard !path.hasPrefix("/"), !drivePath, !path.contains("://"),
179
+ parts.allSatisfy({ !$0.isEmpty && $0 != "." && $0 != ".." })
180
+ else {
181
+ throw issueValidationError("issue evidence likelyFixArea file must be a safe relative path")
182
+ }
183
+ return path
184
+ }
@@ -101,6 +101,7 @@ public extension IssueAttributes {
101
101
  handled: Bool = true,
102
102
  breadcrumbs: [IssueBreadcrumb]? = nil,
103
103
  breadcrumbsTruncated: Bool? = nil,
104
+ evidence: IssueDiagnosticEvidence? = nil,
104
105
  metadata: Metadata? = nil,
105
106
  context: TelemetryContext? = nil,
106
107
  fileID: String = #fileID,
@@ -136,6 +137,7 @@ public extension IssueAttributes {
136
137
  stackFrames: [rootFrame],
137
138
  breadcrumbs: breadcrumbs,
138
139
  breadcrumbsTruncated: breadcrumbsTruncated,
140
+ evidence: evidence,
139
141
  metadata: metadata,
140
142
  context: context,
141
143
  )
@@ -186,6 +188,7 @@ func normalizeIssueAttributes(_ value: IssueAttributes) throws -> IssueAttribute
186
188
  }
187
189
  let breadcrumbs = try normalizeIssueBreadcrumbs(value.breadcrumbs)
188
190
  let breadcrumbsWereCapped = (value.breadcrumbs?.count ?? 0) > maximumIssueBreadcrumbs
191
+ let evidence = try value.evidence.map(validateIssueDiagnosticEvidence)
189
192
  let context = try value.context.map { try validateTelemetryContext($0) }
190
193
  try validateMetadata(value.metadata, label: "issue metadata")
191
194
  return IssueAttributes(
@@ -197,6 +200,7 @@ func normalizeIssueAttributes(_ value: IssueAttributes) throws -> IssueAttribute
197
200
  stackFrames: stackFrames,
198
201
  breadcrumbs: breadcrumbs,
199
202
  breadcrumbsTruncated: breadcrumbsWereCapped ? true : value.breadcrumbsTruncated,
203
+ evidence: evidence,
200
204
  metadata: value.metadata,
201
205
  context: context,
202
206
  nativeStackFrames: value.nativeStackFrames,
@@ -224,6 +228,7 @@ func issueAttributes(
224
228
  breadcrumbsTruncated: value.breadcrumbsTruncated == true
225
229
  || snapshot.truncated
226
230
  || combined.count > maximumIssueBreadcrumbs,
231
+ evidence: value.evidence,
227
232
  metadata: value.metadata,
228
233
  context: value.context,
229
234
  nativeStackFrames: value.nativeStackFrames,
@@ -377,7 +382,7 @@ func issueText(
377
382
  disallowLocationDelimiters: Bool = false,
378
383
  ) throws -> String {
379
384
  let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
380
- guard !normalized.isEmpty, normalized.count <= maximum, !containsForbiddenControl(normalized),
385
+ guard !normalized.isEmpty, normalized.unicodeScalars.count <= maximum, !containsForbiddenControl(normalized),
381
386
  !disallowLocationDelimiters || (!normalized.contains("?") && !normalized.contains("#"))
382
387
  else {
383
388
  throw issueValidationError("\(label) is invalid")
@@ -64,7 +64,6 @@ public final class LogBrewClient: @unchecked Sendable {
64
64
  let context = try resolvedContext(event: attributes.context)
65
65
  let attributes = issueAttributes(attributes, adding: breadcrumbStore.snapshot())
66
66
  .withContext(context)
67
- .withCorrelationMetadata()
68
67
  try pushEvent(.issue(validateIssue(attributes)), id: id, timestamp: timestamp)
69
68
  }
70
69
 
@@ -76,7 +75,7 @@ public final class LogBrewClient: @unchecked Sendable {
76
75
  public func log(_ id: String, timestamp: String, attributes: LogAttributes) throws {
77
76
  let context = try resolvedContext(event: attributes.context)
78
77
  try pushEvent(
79
- .log(validateLog(attributes.withContext(context).withCorrelationMetadata())),
78
+ .log(validateLog(attributes.withContext(context))),
80
79
  id: id,
81
80
  timestamp: timestamp,
82
81
  )
@@ -103,7 +102,7 @@ public final class LogBrewClient: @unchecked Sendable {
103
102
  public func action(_ id: String, timestamp: String, attributes: ActionAttributes) throws {
104
103
  let context = try resolvedContext(event: attributes.context)
105
104
  try pushEvent(
106
- .action(validateAction(attributes.withContext(context).withCorrelationMetadata())),
105
+ .action(validateAction(attributes.withContext(context))),
107
106
  id: id,
108
107
  timestamp: timestamp,
109
108
  )
@@ -112,7 +111,7 @@ public final class LogBrewClient: @unchecked Sendable {
112
111
  public func metric(_ id: String, timestamp: String, attributes: MetricAttributes) throws {
113
112
  let context = try resolvedContext(event: attributes.context)
114
113
  try pushEvent(
115
- .metric(validateMetric(attributes.withContext(context).withCorrelationMetadata())),
114
+ .metric(validateMetric(attributes.withContext(context))),
116
115
  id: id,
117
116
  timestamp: timestamp,
118
117
  )
@@ -209,22 +208,7 @@ private extension IssueAttributes {
209
208
  stackFrames: stackFrames,
210
209
  breadcrumbs: breadcrumbs,
211
210
  breadcrumbsTruncated: breadcrumbsTruncated,
212
- metadata: metadata,
213
- context: context,
214
- nativeStackFrames: nativeStackFrames,
215
- )
216
- }
217
-
218
- func withCorrelationMetadata() -> IssueAttributes {
219
- IssueAttributes(
220
- title: title,
221
- level: level,
222
- message: message,
223
- exception: exception,
224
- exceptionChain: exceptionChain,
225
- stackFrames: stackFrames,
226
- breadcrumbs: breadcrumbs,
227
- breadcrumbsTruncated: breadcrumbsTruncated,
211
+ evidence: evidence,
228
212
  metadata: correlationMetadata(metadata, context: context),
229
213
  context: context,
230
214
  nativeStackFrames: nativeStackFrames,
@@ -234,16 +218,6 @@ private extension IssueAttributes {
234
218
 
235
219
  private extension LogAttributes {
236
220
  func withContext(_ context: TelemetryContext?) -> LogAttributes {
237
- LogAttributes(
238
- message: message,
239
- level: level,
240
- logger: logger,
241
- metadata: metadata,
242
- context: context,
243
- )
244
- }
245
-
246
- func withCorrelationMetadata() -> LogAttributes {
247
221
  LogAttributes(
248
222
  message: message,
249
223
  level: level,
@@ -256,10 +230,6 @@ private extension LogAttributes {
256
230
 
257
231
  private extension ActionAttributes {
258
232
  func withContext(_ context: TelemetryContext?) -> ActionAttributes {
259
- ActionAttributes(name: name, status: status, metadata: metadata, context: context)
260
- }
261
-
262
- func withCorrelationMetadata() -> ActionAttributes {
263
233
  ActionAttributes(
264
234
  name: name,
265
235
  status: status,
@@ -271,19 +241,6 @@ private extension ActionAttributes {
271
241
 
272
242
  private extension MetricAttributes {
273
243
  func withContext(_ context: TelemetryContext?) -> MetricAttributes {
274
- MetricAttributes(
275
- name: name,
276
- kind: kind,
277
- value: value,
278
- unit: unit,
279
- temporality: temporality,
280
- metadata: metadata,
281
- context: context,
282
- description: description,
283
- )
284
- }
285
-
286
- func withCorrelationMetadata() -> MetricAttributes {
287
244
  MetricAttributes(
288
245
  name: name,
289
246
  kind: kind,
@@ -175,6 +175,7 @@ public struct IssueAttributes: Codable, Equatable, Sendable {
175
175
  public let stackFrames: [IssueStackFrame]?
176
176
  public let breadcrumbs: [IssueBreadcrumb]?
177
177
  public let breadcrumbsTruncated: Bool?
178
+ public let evidence: IssueDiagnosticEvidence?
178
179
  public let metadata: Metadata?
179
180
  public let context: TelemetryContext?
180
181
  @_spi(CrashReplay) public let nativeStackFrames: [NativeStackFrame]?
@@ -188,6 +189,7 @@ public struct IssueAttributes: Codable, Equatable, Sendable {
188
189
  stackFrames: [IssueStackFrame]? = nil,
189
190
  breadcrumbs: [IssueBreadcrumb]? = nil,
190
191
  breadcrumbsTruncated: Bool? = nil,
192
+ evidence: IssueDiagnosticEvidence? = nil,
191
193
  metadata: Metadata? = nil,
192
194
  context: TelemetryContext? = nil,
193
195
  ) {
@@ -199,6 +201,7 @@ public struct IssueAttributes: Codable, Equatable, Sendable {
199
201
  self.stackFrames = stackFrames
200
202
  self.breadcrumbs = breadcrumbs
201
203
  self.breadcrumbsTruncated = breadcrumbsTruncated
204
+ self.evidence = evidence
202
205
  self.metadata = metadata
203
206
  self.context = context
204
207
  nativeStackFrames = nil
@@ -214,6 +217,7 @@ public struct IssueAttributes: Codable, Equatable, Sendable {
214
217
  stackFrames: [IssueStackFrame]? = nil,
215
218
  breadcrumbs: [IssueBreadcrumb]? = nil,
216
219
  breadcrumbsTruncated: Bool? = nil,
220
+ evidence: IssueDiagnosticEvidence? = nil,
217
221
  metadata: Metadata? = nil,
218
222
  context: TelemetryContext? = nil,
219
223
  nativeStackFrames: [NativeStackFrame]?,
@@ -226,6 +230,7 @@ public struct IssueAttributes: Codable, Equatable, Sendable {
226
230
  self.stackFrames = stackFrames
227
231
  self.breadcrumbs = breadcrumbs
228
232
  self.breadcrumbsTruncated = breadcrumbsTruncated
233
+ self.evidence = evidence
229
234
  self.metadata = metadata
230
235
  self.context = context
231
236
  self.nativeStackFrames = nativeStackFrames
@@ -81,9 +81,14 @@ func validateTelemetryTraceContext(
81
81
  )
82
82
  }
83
83
 
84
- func validMachineKey(_ value: String, maximum: Int, separators: Set<Character>) -> Bool {
84
+ func validMachineKey(
85
+ _ value: String,
86
+ maximum: Int,
87
+ separators: Set<Character>,
88
+ allowNumericStart: Bool = false,
89
+ ) -> Bool {
85
90
  guard !value.isEmpty, value.count <= maximum, value.first?.isASCII == true,
86
- value.first?.isLetter == true
91
+ value.first?.isLetter == true || allowNumericStart && value.first?.isNumber == true
87
92
  else {
88
93
  return false
89
94
  }
@@ -28,6 +28,10 @@ struct CrashReportSanitizer {
28
28
  mechanism: mechanism(error?["type"] as? String),
29
29
  nativeStackFrames: nativeStackFrames,
30
30
  artifactIdentity: artifactIdentity,
31
+ context: nativeCrashContext(
32
+ system: rawReport["system"] as? [String: Any],
33
+ artifactIdentity: artifactIdentity,
34
+ ),
31
35
  hangState: nil,
32
36
  hangDurationMs: nil,
33
37
  source: .engine(reportID: reportID),
@@ -70,6 +74,55 @@ struct CrashReportSanitizer {
70
74
  }
71
75
  }
72
76
 
77
+ func nativeCrashContext(
78
+ system: [String: Any]?,
79
+ artifactIdentity: NativeArtifactIdentity?,
80
+ ) -> TelemetryContext? {
81
+ let value = { (key: String) in crashContextValue(system?[key]) }
82
+ let operatingSystem = value("system_name").map {
83
+ TelemetryOperatingSystem(name: $0, version: value("system_version"), build: value("os_version"))
84
+ }
85
+ let family = value("model")
86
+ let model = value("machine")
87
+ let architecture = value("cpu_arch")
88
+ let device = family == nil && model == nil && architecture == nil
89
+ ? nil
90
+ : TelemetryDevice(family: family, model: model, architecture: architecture)
91
+ let applicationName = value("CFBundleName")
92
+ let applicationVersion = value("CFBundleShortVersionString")
93
+ let applicationBuild = value("CFBundleVersion")
94
+ let application = applicationName == nil && applicationVersion == nil && applicationBuild == nil
95
+ ? nil
96
+ : TelemetryApplication(name: applicationName, version: applicationVersion, build: applicationBuild)
97
+ guard artifactIdentity != nil || operatingSystem != nil || device != nil || application != nil else {
98
+ return nil
99
+ }
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
+ ))
109
+ }
110
+
111
+ private func crashContextValue(_ rawValue: Any?) -> String? {
112
+ guard let value = rawValue as? String else {
113
+ return nil
114
+ }
115
+ let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
116
+ guard (1 ... 256).contains(normalized.utf8.count),
117
+ !normalized.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains),
118
+ !normalized.contains("/"),
119
+ !normalized.contains("\\")
120
+ else {
121
+ return nil
122
+ }
123
+ return normalized
124
+ }
125
+
73
126
  struct NativeCrashTimestamp: Comparable {
74
127
  let normalized: String
75
128
  private let wholeSecond: String
@@ -1,6 +1,5 @@
1
1
  // Generated by scripts/sync-apple-native-sources.mjs.
2
2
  // Edit the canonical Swift source, then regenerate this package boundary.
3
- import Foundation
4
3
 
5
4
  func nativeCrashExceptionChain(for exception: IssueException) -> IssueExceptionChain {
6
5
  IssueExceptionChain(entries: [
@@ -14,31 +13,3 @@ func nativeCrashExceptionChain(for exception: IssueException) -> IssueExceptionC
14
13
  ),
15
14
  ])
16
15
  }
17
-
18
- func nativeCrashExceptionChainMatches(
19
- _ value: Any?,
20
- expected exception: IssueException,
21
- ) -> Bool {
22
- guard let value else {
23
- // Preserve retry identity for an event queued by an older SDK before chains existed.
24
- return true
25
- }
26
- guard let chain = value as? [String: Any] else {
27
- return false
28
- }
29
- guard let mechanism = exception.mechanism else {
30
- return false
31
- }
32
- let expected = [
33
- "entries": [[
34
- "id": 0,
35
- "relationship": "reported",
36
- "type": exception.type,
37
- "messageState": "not_captured",
38
- "mechanism": ["type": mechanism.type, "handled": mechanism.handled],
39
- "stackFramesState": "not_captured",
40
- ]],
41
- "truncated": false,
42
- ] as NSDictionary
43
- return chain as NSDictionary == expected
44
- }
@@ -163,6 +163,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
163
163
 
164
164
  private let nativeStackFrames: [NativeStackFrame]?
165
165
  private let artifactIdentity: NativeArtifactIdentity?
166
+ private let context: TelemetryContext?
166
167
  private let hangState: NativeHangIncidentState?
167
168
  private let hangDurationMs: Double?
168
169
  let source: NativeCrashRecordSource
@@ -175,6 +176,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
175
176
  mechanism: NativeCrashMechanism,
176
177
  nativeStackFrames: [NativeStackFrame]?,
177
178
  artifactIdentity: NativeArtifactIdentity?,
179
+ context: TelemetryContext?,
178
180
  hangState: NativeHangIncidentState?,
179
181
  hangDurationMs: Double? = nil,
180
182
  source: NativeCrashRecordSource,
@@ -186,6 +188,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
186
188
  self.mechanism = mechanism
187
189
  self.nativeStackFrames = nativeStackFrames
188
190
  self.artifactIdentity = artifactIdentity
191
+ self.context = context
189
192
  self.hangState = hangState
190
193
  self.hangDurationMs = hangDurationMs
191
194
  self.source = source
@@ -237,6 +240,7 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
237
240
  exception: issueException,
238
241
  exceptionChain: nativeCrashExceptionChain(for: issueException),
239
242
  metadata: metadata,
243
+ context: context,
240
244
  nativeStackFrames: nativeStackFrames,
241
245
  )
242
246
  }
@@ -254,57 +258,13 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
254
258
  guard event["type"] as? String == "issue",
255
259
  event["timestamp"] as? String == timestamp,
256
260
  let attributes = event["attributes"] as? [String: Any],
257
- attributes["title"] as? String == issueAttributes.title,
258
- attributes["level"] as? String == issueAttributes.level.canonicalValue,
259
- attributes["message"] == nil,
260
- exceptionMatches(attributes["exception"]),
261
- nativeCrashExceptionChainMatches(
262
- attributes["exceptionChain"],
263
- expected: issueException,
264
- ),
265
- attributes["stackFrames"] == nil,
266
- attributes["breadcrumbs"] == nil,
267
- attributes["breadcrumbsTruncated"] == nil,
268
- attributes["context"] == nil,
269
- let metadata = attributes["metadata"] as? [String: Any],
270
- metadata as NSDictionary == expectedMetadata,
271
- nativeStackFramesMatch(attributes["nativeStackFrames"])
261
+ attributesMatch(attributes)
272
262
  else {
273
263
  return .collision
274
264
  }
275
265
  return .matching
276
266
  }
277
267
 
278
- private var expectedMetadata: NSDictionary {
279
- var metadata: [String: Any] = [
280
- "crash.mechanism": mechanism.name,
281
- "crash.replayed": true,
282
- ]
283
- if let artifactIdentity {
284
- metadata["projectId"] = artifactIdentity.projectId
285
- metadata["release"] = artifactIdentity.release
286
- metadata["environment"] = artifactIdentity.environment
287
- metadata["service"] = artifactIdentity.service
288
- }
289
- if let hangState {
290
- metadata["crash.handled"] = hangState == .recovered
291
- }
292
- if let hangDurationMs {
293
- metadata["durationMs"] = hangDurationMs
294
- }
295
- return metadata as NSDictionary
296
- }
297
-
298
- private var expectedException: NSDictionary {
299
- [
300
- "type": hangState == nil ? "AppleNativeCrash" : "AppleNativeHang",
301
- "mechanism": [
302
- "type": mechanism.name,
303
- "handled": hangState == .recovered,
304
- ],
305
- ] as NSDictionary
306
- }
307
-
308
268
  private var issueException: IssueException {
309
269
  IssueException(
310
270
  type: hangState == nil ? "AppleNativeCrash" : "AppleNativeHang",
@@ -315,31 +275,16 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
315
275
  )
316
276
  }
317
277
 
318
- private func exceptionMatches(_ value: Any?) -> Bool {
319
- guard let value else {
320
- // Preserve exact retry identity for an event queued by an older SDK before typed exceptions existed.
321
- return true
322
- }
323
- guard let exception = value as? [String: Any] else {
324
- return false
325
- }
326
- return exception as NSDictionary == expectedException
327
- }
328
-
329
- private func nativeStackFramesMatch(_ value: Any?) -> Bool {
330
- guard let nativeStackFrames else {
331
- return value == nil
332
- }
333
- guard let frames = value as? [[String: Any]], frames.count == nativeStackFrames.count else {
278
+ private func attributesMatch(_ actual: [String: Any]) -> Bool {
279
+ guard let encoded = try? JSONEncoder().encode(issueAttributes),
280
+ var expected = try? JSONSerialization.jsonObject(with: encoded) as? [String: Any]
281
+ else {
334
282
  return false
335
283
  }
336
- return zip(frames, nativeStackFrames).allSatisfy { frame, expected in
337
- frame as NSDictionary == [
338
- "imageUuid": expected.imageUuid,
339
- "architecture": expected.architecture.rawValue,
340
- "instructionOffset": expected.instructionOffset,
341
- ] as NSDictionary
284
+ for legacyField in ["exception", "exceptionChain", "context"] where actual[legacyField] == nil {
285
+ expected.removeValue(forKey: legacyField)
342
286
  }
287
+ return actual as NSDictionary == expected as NSDictionary
343
288
  }
344
289
  }
345
290
 
@@ -93,6 +93,7 @@ struct NativeHangIncident: Codable, Equatable {
93
93
  mechanism: .deadlock,
94
94
  nativeStackFrames: nativeStackFrames,
95
95
  artifactIdentity: artifactIdentity.validatedIdentity(),
96
+ context: nil,
96
97
  hangState: state,
97
98
  hangDurationMs: durationMs,
98
99
  source: .hang(eventID: eventID),
@@ -7,35 +7,36 @@
7
7
  "swift/logbrew-swift/Sources/LogBrew/DeliveryEngineDurable.swift": "b0d2ca07cce7fe01fc110238c7d10aa9c8ada0d28509e4c022e7fa950a4c79b9",
8
8
  "swift/logbrew-swift/Sources/LogBrew/DeliveryEngineQueue.swift": "9c8503da4023368922b8b73dd989213685a64312806ecd73d0513958ad85f883",
9
9
  "swift/logbrew-swift/Sources/LogBrew/DeliveryLifecycle.swift": "7b76541c2659cecf6614d9d6dcf9b61dc85b7a562d44fda03a026076afa02fcd",
10
- "swift/logbrew-swift/Sources/LogBrew/DurableDeliveryStore.swift": "2de6a08473722ebd99b2d1762ed3356fefc0181f5cd886367fbfd52c75c6644b",
11
- "swift/logbrew-swift/Sources/LogBrew/DurableDeliveryStoreRecovery.swift": "a843f784e7645f916bb0f401e88fa7d49fa8c4351dc2f26c1fbf32be3f4aaa40",
10
+ "swift/logbrew-swift/Sources/LogBrew/DurableDeliveryStore.swift": "7c292dda68efc3de790daf44733b25e4392226793343c21ba5df4f51fc1b402b",
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/IssueDiagnostics.swift": "fc805effd3da6084e8cac2bbfe8fdc098e07d45d6f3ce2e7df02ebe0e445591f",
13
+ "swift/logbrew-swift/Sources/LogBrew/IssueDiagnosticEvidence.swift": "e471f27fadaefe06971c1a98892aeb0774e840a2ce0cbb243ae9aabfa41b1c47",
14
+ "swift/logbrew-swift/Sources/LogBrew/IssueDiagnostics.swift": "297b8bbefd42c314a984fecb38237fb1395700c9eafcfdd1b545267bc34294cc",
14
15
  "swift/logbrew-swift/Sources/LogBrew/IssueExceptionChain.swift": "10f81cbce95b47da0a44ef9e515b150e60d71dfcc8b04be24b037b1222a373a5",
15
16
  "swift/logbrew-swift/Sources/LogBrew/LifecycleTrace.swift": "738cd3f129fac821a892404d1d42ee92bd806b6583d3dbb5379e88b14ab23c16",
16
- "swift/logbrew-swift/Sources/LogBrew/LogBrewClient.swift": "0f7e23a21fd8f76af031205847cd0b8b2648a069abac8d0ddc8c16da017893b7",
17
+ "swift/logbrew-swift/Sources/LogBrew/LogBrewClient.swift": "186cdbda3754a811f17b82818ca8e7a1711b3b75123686cd17dc3b7441fa21cb",
17
18
  "swift/logbrew-swift/Sources/LogBrew/LogBrewLogger.swift": "d3266353a5f5139a2a1ca1a86bacce793518eb1489bf67ed9e3720b57b1c29b2",
18
19
  "swift/logbrew-swift/Sources/LogBrew/LogBrewTrace.swift": "f67edf4ca464e6a6eb1c7a22cd4997928cd27b6c2d07535855a18a053d8de924",
19
20
  "swift/logbrew-swift/Sources/LogBrew/Metadata.swift": "65840ebb3f3b79fbfc1f5faa3026c2583319e89ff26061676dc552c03ee15dbe",
20
21
  "swift/logbrew-swift/Sources/LogBrew/NativeStackFrame.swift": "ecbd418969ec4a31503d9916a8bf27f22e6863356c6fabe457c1e61027a8d805",
21
22
  "swift/logbrew-swift/Sources/LogBrew/ProductTimeline.swift": "209906aa0e8f096a347e35832840522710eadfb530053253cd73c748518e362d",
22
- "swift/logbrew-swift/Sources/LogBrew/PublicTypes.swift": "1151d5402a9f5441f5d5d998fde57c8df88cb3180d2daa3e0fc49e883ab274f2",
23
+ "swift/logbrew-swift/Sources/LogBrew/PublicTypes.swift": "a21aee058f716ec9d41d54ad032b2c7a4d64a4b99d749a9fa2be3fd2df5c6568",
23
24
  "swift/logbrew-swift/Sources/LogBrew/TelemetryContext.swift": "ce3ac93a62a345f8c9a8f4c69ce84efc991bf02a688f49bad5e68edcff2f6e08",
24
- "swift/logbrew-swift/Sources/LogBrew/TelemetryContextValidation.swift": "f631beef20a8a86f975787b4092a9798fedbe2c9e3f0d2740f81831b4adef287",
25
+ "swift/logbrew-swift/Sources/LogBrew/TelemetryContextValidation.swift": "23524d281a185962a2ecc75f1ca1cb775171e2f72ecd39ca325ace7e45082485",
25
26
  "swift/logbrew-swift/Sources/LogBrew/TraceEvidence.swift": "40aaa42976164c53d9f4d9e9e30d236534238da43bbe90832a673c8809ad54b6",
26
27
  "swift/logbrew-swift/Sources/LogBrew/Transport.swift": "73eec81aaebec4bc922b2a372429c88fcd3e85606a7862cd08e1795eb2a8e6cd",
27
28
  "swift/logbrew-swift/Sources/LogBrew/URLSessionTrace.swift": "d0076c72671716c41bd7498d0509e453b8182c418156e169196fee9f26fc66b1",
28
29
  "swift/logbrew-swift/Sources/LogBrew/URLSessionTracer.swift": "0cdf08f92d6111c0dc2883b87837b81f69b5c2f9a8e5501780a114c23a741af7",
29
30
  "swift/logbrew-swift/Sources/LogBrew/Validation.swift": "07ff5c2daeb829b1df4f50edb0b79727f9a81b1c9b5cde5f995f5ad6ce660aa7",
30
31
  "swift/logbrew-swift/Sources/LogBrewCrash/CrashEngine.swift": "7a46035fcaa72dd7504b4e274a92070d73dd1c45fe20e008474340d6d7c44ede",
31
- "swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "a872ad63532999e74af1e9685a9382d0e02b93c6f9b942b371d47f56f8b961c0",
32
+ "swift/logbrew-swift/Sources/LogBrewCrash/CrashReportSanitizer.swift": "8798c8900ea28a85d6909183dc8435bc08da96573b89310d26ed178217fbc04b",
32
33
  "swift/logbrew-swift/Sources/LogBrewCrash/CrashStorageDirectory.swift": "7cd566703cbf704dc99155451ee93dcace12c35c805abc09a6dcf23975cf43f9",
33
34
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeArtifactIdentity.swift": "3d785ad717dcf7302451531c18b5e1183983270a6cd78dd543a407e3facc6ae0",
34
35
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCapture.swift": "e9ab5f1850a096f40592a025d16c5ffa2fdff0222004a37056898cf1cb4b0dd9",
35
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashExceptionEvidence.swift": "08819652ce0796bee9f502de1202d236887afbcb7794d4db9adc5b6c2928ca2c",
36
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "fe0db58c4ad348a2601d984cdf097147f0b2093baad64861aef48cc052697d9c",
36
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashExceptionEvidence.swift": "ce191e46298d91d87b379528d610caa88b924b15a8dd912b4a164b51eb172a7a",
37
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashPublic.swift": "7c1d028bea143c6ab34ebddaaaab64aeb69d4d950ec7a42264c567e672991748",
37
38
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashReplay.swift": "3eb3d93b32cf14de416f56fa30d6d65e6f268b5ec9d1a67081d7525c4efd4f80",
38
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangIncidentStore.swift": "02413ca7e003917faadcc9aa612b5d98897c75b39f67c3c845948dd9349a9d8f",
39
+ "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangIncidentStore.swift": "5a8150083f16d6b495c9c63434809dc5f11f13ebb23211f57151bad62040781b",
39
40
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdog.swift": "37b25904cd6576e20c40500be2c153156403b57ad7b4186a1b6b35b414145ecb",
40
41
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdogContracts.swift": "59f51554d86c326122bb2bdce47c5a7e01996bd61f2b371eba23855032487b9f",
41
42
  "swift/logbrew-swift/Sources/LogBrewCrash/NativeHangWatchdogSystem.swift": "b87e3db246691d5b0719898cdc02ba068836647b1ac9339a73cf866eabe235ae",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/react-native",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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",