@logbrew/react-native 0.1.20 → 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.
@@ -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
  }
@@ -174,7 +174,7 @@ final class IssueBreadcrumbStore: @unchecked Sendable {
174
174
  }
175
175
  }
176
176
 
177
- let maximumIssueBreadcrumbs = 64
177
+ @_spi(CrashReplay) public let maximumIssueBreadcrumbs = 64
178
178
 
179
179
  func normalizeIssueAttributes(_ value: IssueAttributes) throws -> IssueAttributes {
180
180
  let exception = try value.exception.map(validateIssueException)
@@ -251,7 +251,8 @@ func validateIssueException(_ value: IssueException) throws -> IssueException {
251
251
  return IssueException(type: exceptionType, mechanism: mechanism)
252
252
  }
253
253
 
254
- func validateIssueBreadcrumb(_ value: IssueBreadcrumb) throws -> IssueBreadcrumb {
254
+ @_spi(CrashReplay)
255
+ public func validateIssueBreadcrumb(_ value: IssueBreadcrumb) throws -> IssueBreadcrumb {
255
256
  try requireTimestamp(value.timestamp)
256
257
  guard validMachineKey(value.category, maximum: 64, separators: ["_", ".", ":", "-"]) else {
257
258
  throw issueValidationError("issue breadcrumb category is invalid")
@@ -23,6 +23,10 @@ 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
29
+ let breadcrumbs = NativeCrashBreadcrumbs.captured(in: rawReport)
26
30
  return NativeCrashRecord(
27
31
  eventID: uuid.uuidString.lowercased(),
28
32
  timestamp: timestamp.normalized,
@@ -32,9 +36,13 @@ struct CrashReportSanitizer {
32
36
  context: nativeCrashContext(
33
37
  system: rawReport["system"] as? [String: Any],
34
38
  artifactIdentity: artifactIdentity,
35
- correlation: correlation.0,
39
+ correlation: correlation.0?.correlationContext,
36
40
  ),
37
- correlationState: correlation.1,
41
+ correlationState: correlationState,
42
+ impact: correlation.0?.impact,
43
+ breadcrumbs: breadcrumbs.0?.breadcrumbs,
44
+ breadcrumbsTruncated: breadcrumbs.0?.truncated,
45
+ breadcrumbState: breadcrumbs.1,
38
46
  hangState: nil,
39
47
  hangDurationMs: nil,
40
48
  source: .engine(reportID: reportID),
@@ -109,6 +109,34 @@ 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
+ 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)
122
+ }
123
+ }
124
+
125
+ /// Replaces the crash-time breadcrumb snapshot in one bounded write.
126
+ @nonobjc
127
+ public func setBreadcrumbs(
128
+ _ breadcrumbs: [IssueBreadcrumb]?,
129
+ truncated: Bool = false,
130
+ ) throws {
131
+ 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?,
139
+ ) throws {
112
140
  lock.lock()
113
141
  defer { lock.unlock() }
114
142
  try verifyProcessLocked()
@@ -116,10 +144,7 @@ public final class NativeCrashCapture: NSObject, @unchecked Sendable {
116
144
  throw NativeCrashError(.notInstalled)
117
145
  }
118
146
  try verifyStorageLocked()
119
- try driver.setUserInfo(
120
- NativeCrashCorrelation.encoded(context),
121
- forKey: NativeCrashCorrelation.reportKey,
122
- )
147
+ try driver.setUserInfo(encoded(), forKey: key)
123
148
  }
124
149
 
125
150
  @objc(replayPendingReportsWithHandler:error:)
@@ -2,76 +2,207 @@
2
2
  // Edit the canonical Swift source, then regenerate this package boundary.
3
3
  import Foundation
4
4
 
5
- enum NativeCrashCorrelationState: String {
5
+ enum NativeCrashContextState: String {
6
6
  case captured
7
7
  case notCaptured = "not_captured"
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
  }
24
68
  return value
25
- } catch let error as NativeCrashError {
26
- throw error
27
69
  } catch {
28
70
  throw NativeCrashError(.invalidConfiguration)
29
71
  }
30
72
  }
31
73
 
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)
74
+ static func captured(
75
+ in rawReport: [String: Any],
76
+ ) -> (NativeCrashDiagnosticSnapshot?, NativeCrashContextState) {
77
+ nativeCrashCaptured(in: rawReport, key: reportKey, maximumBytes: maximumBytes, validate: validated)
45
78
  }
46
79
 
47
- static func validated(_ object: Any) throws -> TelemetryContext {
48
- do {
49
- guard JSONSerialization.isValidJSONObject(object) else {
80
+ static func validated(_ object: Any) throws -> NativeCrashDiagnosticSnapshot {
81
+ try nativeCrashValidated(object, maximumBytes: maximumBytes) { decoded in
82
+ guard decoded.schemaVersion == 1 else {
50
83
  throw NativeCrashError(.invalidConfiguration)
51
84
  }
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),
85
+ return try NativeCrashDiagnosticSnapshot(
86
+ context: decoded.correlationContext,
87
+ impact: decoded.impact,
58
88
  )
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)
89
+ }
90
+ }
91
+ }
92
+
93
+ struct NativeCrashBreadcrumbSnapshot: Codable, Equatable {
94
+ let schemaVersion: Int
95
+ let breadcrumbs: [IssueBreadcrumb]
96
+ let truncated: Bool
97
+
98
+ init(breadcrumbs: [IssueBreadcrumb], truncated: Bool) {
99
+ schemaVersion = 1
100
+ self.breadcrumbs = breadcrumbs
101
+ self.truncated = truncated
102
+ }
103
+ }
104
+
105
+ enum NativeCrashBreadcrumbs {
106
+ static let reportKey = "logbrew_native_breadcrumbs"
107
+ static let maximumBytes = 64 * 1024
108
+
109
+ static func encoded(_ values: [IssueBreadcrumb]?, truncated: Bool) throws -> String? {
110
+ guard let values, !values.isEmpty else {
111
+ return nil
112
+ }
113
+ do {
114
+ var breadcrumbs = try values.suffix(maximumIssueBreadcrumbs).map(validateIssueBreadcrumb)
115
+ var wasTruncated = truncated || breadcrumbs.count < values.count
116
+ while !breadcrumbs.isEmpty {
117
+ let data = try nativeCrashEncoded(NativeCrashBreadcrumbSnapshot(
118
+ breadcrumbs: breadcrumbs,
119
+ truncated: wasTruncated,
120
+ ))
121
+ if data.count <= maximumBytes, let value = String(data: data, encoding: .utf8) {
122
+ return value
123
+ }
124
+ breadcrumbs.removeFirst()
125
+ wasTruncated = true
63
126
  }
64
- return context
65
127
  } catch let error as NativeCrashError {
66
128
  throw error
67
129
  } catch {
68
130
  throw NativeCrashError(.invalidConfiguration)
69
131
  }
132
+ throw NativeCrashError(.invalidConfiguration)
133
+ }
134
+
135
+ static func captured(
136
+ in rawReport: [String: Any],
137
+ ) -> (NativeCrashBreadcrumbSnapshot?, NativeCrashContextState) {
138
+ nativeCrashCaptured(in: rawReport, key: reportKey, maximumBytes: maximumBytes, validate: validated)
70
139
  }
71
140
 
72
- private static func encoder() -> JSONEncoder {
73
- let encoder = JSONEncoder()
74
- encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
75
- return encoder
141
+ static func validated(_ object: Any) throws -> NativeCrashBreadcrumbSnapshot {
142
+ try nativeCrashValidated(object, maximumBytes: maximumBytes) { decoded in
143
+ guard decoded.schemaVersion == 1,
144
+ !decoded.breadcrumbs.isEmpty,
145
+ decoded.breadcrumbs.count <= maximumIssueBreadcrumbs
146
+ else {
147
+ throw NativeCrashError(.invalidConfiguration)
148
+ }
149
+ return try NativeCrashBreadcrumbSnapshot(
150
+ breadcrumbs: decoded.breadcrumbs.map(validateIssueBreadcrumb),
151
+ truncated: decoded.truncated,
152
+ )
153
+ }
76
154
  }
77
155
  }
156
+
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)
201
+ }
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
+ }
@@ -164,7 +164,11 @@ 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
+ private let correlationState: NativeCrashContextState?
168
+ private let impact: IssueImpactEvidence?
169
+ private let breadcrumbs: [IssueBreadcrumb]?
170
+ private let breadcrumbsTruncated: Bool?
171
+ private let breadcrumbState: NativeCrashContextState?
168
172
  private let hangState: NativeHangIncidentState?
169
173
  private let hangDurationMs: Double?
170
174
  let source: NativeCrashRecordSource
@@ -178,7 +182,11 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
178
182
  nativeStackFrames: [NativeStackFrame]?,
179
183
  artifactIdentity: NativeArtifactIdentity?,
180
184
  context: TelemetryContext?,
181
- correlationState: NativeCrashCorrelationState? = nil,
185
+ correlationState: NativeCrashContextState? = nil,
186
+ impact: IssueImpactEvidence? = nil,
187
+ breadcrumbs: [IssueBreadcrumb]? = nil,
188
+ breadcrumbsTruncated: Bool? = nil,
189
+ breadcrumbState: NativeCrashContextState? = nil,
182
190
  hangState: NativeHangIncidentState?,
183
191
  hangDurationMs: Double? = nil,
184
192
  source: NativeCrashRecordSource,
@@ -192,6 +200,10 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
192
200
  self.artifactIdentity = artifactIdentity
193
201
  self.context = context
194
202
  self.correlationState = correlationState
203
+ self.impact = impact
204
+ self.breadcrumbs = breadcrumbs
205
+ self.breadcrumbsTruncated = breadcrumbsTruncated
206
+ self.breadcrumbState = breadcrumbState
195
207
  self.hangState = hangState
196
208
  self.hangDurationMs = hangDurationMs
197
209
  self.source = source
@@ -234,6 +246,9 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
234
246
  if let correlationState {
235
247
  metadata["crash.correlation"] = .string(correlationState.rawValue)
236
248
  }
249
+ if let breadcrumbState {
250
+ metadata["crash.breadcrumbs"] = .string(breadcrumbState.rawValue)
251
+ }
237
252
  if let hangState {
238
253
  metadata["crash.handled"] = .bool(hangState == .recovered)
239
254
  }
@@ -245,6 +260,9 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
245
260
  level: hangState == .recovered ? .error : .fatal,
246
261
  exception: issueException,
247
262
  exceptionChain: nativeCrashExceptionChain(for: issueException),
263
+ breadcrumbs: breadcrumbs,
264
+ breadcrumbsTruncated: breadcrumbsTruncated,
265
+ evidence: impact.map { IssueDiagnosticEvidence(impact: $0) },
248
266
  metadata: metadata,
249
267
  context: context,
250
268
  nativeStackFrames: nativeStackFrames,
@@ -287,12 +305,16 @@ public final class NativeCrashRecord: NSObject, @unchecked Sendable {
287
305
  else {
288
306
  return false
289
307
  }
290
- for legacyField in ["exception", "exceptionChain", "context"] where actual[legacyField] == nil {
308
+ let legacyFields = ["exception", "exceptionChain", "evidence", "breadcrumbs", "breadcrumbsTruncated", "context"]
309
+ for legacyField in legacyFields where actual[legacyField] == nil {
291
310
  expected.removeValue(forKey: legacyField)
292
311
  }
293
- if (actual["metadata"] as? [String: Any])?["crash.correlation"] == nil {
312
+ let actualMetadata = actual["metadata"] as? [String: Any]
313
+ if actualMetadata?["crash.correlation"] == nil || actualMetadata?["crash.breadcrumbs"] == nil {
294
314
  var metadata = expected["metadata"] as? [String: Any]
295
- metadata?.removeValue(forKey: "crash.correlation")
315
+ for key in ["crash.correlation", "crash.breadcrumbs"] where actualMetadata?[key] == nil {
316
+ metadata?.removeValue(forKey: key)
317
+ }
296
318
  expected["metadata"] = metadata
297
319
  }
298
320
  return actual as NSDictionary == expected as NSDictionary
@@ -10,8 +10,8 @@
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",
14
- "swift/logbrew-swift/Sources/LogBrew/IssueDiagnostics.swift": "297b8bbefd42c314a984fecb38237fb1395700c9eafcfdd1b545267bc34294cc",
13
+ "swift/logbrew-swift/Sources/LogBrew/IssueDiagnosticEvidence.swift": "1042b5a6283cf440529572844e5ad1f3078f4ba715133932ea305e075614a53c",
14
+ "swift/logbrew-swift/Sources/LogBrew/IssueDiagnostics.swift": "bbdfe6169961ff27b8f2cee63c7dcdeacf2e5cee6a3ca17f6a26b933f0adfc39",
15
15
  "swift/logbrew-swift/Sources/LogBrew/IssueExceptionChain.swift": "10f81cbce95b47da0a44ef9e515b150e60d71dfcc8b04be24b037b1222a373a5",
16
16
  "swift/logbrew-swift/Sources/LogBrew/LifecycleTrace.swift": "738cd3f129fac821a892404d1d42ee92bd806b6583d3dbb5379e88b14ab23c16",
17
17
  "swift/logbrew-swift/Sources/LogBrew/LogBrewClient.swift": "186cdbda3754a811f17b82818ca8e7a1711b3b75123686cd17dc3b7441fa21cb",
@@ -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": "5b503240977bd0c266035b2024e16c50846e6d2932dea083182bc549192a804f",
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": "3ebecef96e67b8e5a56a10d0d847470cb276dea44164e2e637584e110ea3ea39",
36
- "swift/logbrew-swift/Sources/LogBrewCrash/NativeCrashCorrelation.swift": "a513d933eea0b4e83418eb5dd60086cc6871c9e0147a6e7224c45cc6b502de2e",
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": "1672116a06fbdd10971838817591ee23af68bd5a9b07bbffb073e0fafc6cb1c0",
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;