@logbrew/react-native 0.1.10 → 0.1.12
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/LogBrewReactNative.podspec +25 -3
- package/README.md +174 -4
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +611 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +129 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/apple-native-diagnostics.d.ts +52 -0
- package/apple-native-diagnostics.js +286 -0
- package/expo.cjs +46 -0
- package/expo.d.ts +16 -0
- package/expo.js +5 -0
- package/fatal-replay.cjs +12 -1
- package/global-errors.cjs +3 -1
- package/index.cjs +86 -2
- package/index.d.cts +7 -0
- package/index.d.ts +7 -0
- package/index.js +86 -2
- package/index.native.d.ts +47 -1
- package/index.native.js +81 -1
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
- package/ios/LBRNEventRecordStore.h +29 -0
- package/ios/LBRNEventRecordStore.m +746 -0
- package/ios/LBRNFatalStoreModule.mm +159 -14
- package/ios/LBRNPrivateStorage.h +7 -0
- package/ios/LBRNPrivateStorage.m +38 -0
- package/package.json +47 -4
- package/persistent-delivery.native.js +282 -0
- package/promise-rejections.cjs +3 -1
- package/scripts/sync-apple-native-sources.mjs +93 -0
- package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
- package/src/NativeLogBrewFatalStore.ts +9 -0
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
/// Runtime path that captured an exception and whether it escaped that path.
|
|
6
|
+
public struct IssueExceptionMechanism: Codable, Equatable, Sendable {
|
|
7
|
+
public let type: String
|
|
8
|
+
public let handled: Bool
|
|
9
|
+
|
|
10
|
+
public init(type: String, handled: Bool) {
|
|
11
|
+
self.type = type
|
|
12
|
+
self.handled = handled
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Structured exception identity attached to an issue without copying its private description.
|
|
17
|
+
public struct IssueException: Codable, Equatable, Sendable {
|
|
18
|
+
public let type: String
|
|
19
|
+
public let mechanism: IssueExceptionMechanism?
|
|
20
|
+
|
|
21
|
+
public init(type: String, mechanism: IssueExceptionMechanism? = nil) {
|
|
22
|
+
self.type = type
|
|
23
|
+
self.mechanism = mechanism
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Privacy-bounded source identity for one issue stack frame.
|
|
28
|
+
public struct IssueStackFrame: Codable, Equatable, Sendable {
|
|
29
|
+
public let filename: String
|
|
30
|
+
public let line: Int
|
|
31
|
+
public let column: Int
|
|
32
|
+
public let function: String?
|
|
33
|
+
public let module: String?
|
|
34
|
+
public let inApp: Bool?
|
|
35
|
+
public let debugId: String?
|
|
36
|
+
|
|
37
|
+
public init(
|
|
38
|
+
filename: String,
|
|
39
|
+
line: Int,
|
|
40
|
+
column: Int,
|
|
41
|
+
function: String? = nil,
|
|
42
|
+
module: String? = nil,
|
|
43
|
+
inApp: Bool? = nil,
|
|
44
|
+
debugId: String? = nil,
|
|
45
|
+
) {
|
|
46
|
+
self.filename = filename
|
|
47
|
+
self.line = line
|
|
48
|
+
self.column = column
|
|
49
|
+
self.function = function
|
|
50
|
+
self.module = module
|
|
51
|
+
self.inApp = inApp
|
|
52
|
+
self.debugId = debugId
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public enum IssueBreadcrumbLevel: String, Codable, Equatable, Sendable {
|
|
57
|
+
case debug
|
|
58
|
+
case info
|
|
59
|
+
case warning
|
|
60
|
+
case error
|
|
61
|
+
case critical
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// One oldest-to-newest, privacy-bounded step that happened before an issue.
|
|
65
|
+
public struct IssueBreadcrumb: Codable, Equatable, Sendable {
|
|
66
|
+
public let timestamp: String
|
|
67
|
+
public let type: String?
|
|
68
|
+
public let category: String
|
|
69
|
+
public let level: IssueBreadcrumbLevel?
|
|
70
|
+
public let message: String?
|
|
71
|
+
public let data: Metadata?
|
|
72
|
+
|
|
73
|
+
public init(
|
|
74
|
+
timestamp: String,
|
|
75
|
+
category: String,
|
|
76
|
+
type: String? = nil,
|
|
77
|
+
level: IssueBreadcrumbLevel? = nil,
|
|
78
|
+
message: String? = nil,
|
|
79
|
+
data: Metadata? = nil,
|
|
80
|
+
) {
|
|
81
|
+
self.timestamp = timestamp
|
|
82
|
+
self.type = type
|
|
83
|
+
self.category = category
|
|
84
|
+
self.level = level
|
|
85
|
+
self.message = message
|
|
86
|
+
self.data = data
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public extension IssueAttributes {
|
|
91
|
+
/// Create a handled-error issue with safe type identity and a call-site frame.
|
|
92
|
+
///
|
|
93
|
+
/// The error description, raw stack text, locals, arguments, and absolute source paths are
|
|
94
|
+
/// deliberately omitted. Pass `message` only when the application has approved it for display.
|
|
95
|
+
static func fromError(
|
|
96
|
+
_ error: any Error,
|
|
97
|
+
title: String? = nil,
|
|
98
|
+
level: IssueLevel = .error,
|
|
99
|
+
message: String? = nil,
|
|
100
|
+
mechanism: String = "swift.error",
|
|
101
|
+
handled: Bool = true,
|
|
102
|
+
breadcrumbs: [IssueBreadcrumb]? = nil,
|
|
103
|
+
breadcrumbsTruncated: Bool? = nil,
|
|
104
|
+
metadata: Metadata? = nil,
|
|
105
|
+
context: TelemetryContext? = nil,
|
|
106
|
+
fileID: String = #fileID,
|
|
107
|
+
line: Int = #line,
|
|
108
|
+
column: Int = #column,
|
|
109
|
+
function: String = #function,
|
|
110
|
+
) -> IssueAttributes {
|
|
111
|
+
let exceptionType = safeSwiftErrorType(error)
|
|
112
|
+
let fileModule = fileID.split(separator: "/", maxSplits: 1).first.map(String.init)
|
|
113
|
+
return IssueAttributes(
|
|
114
|
+
title: title ?? exceptionType,
|
|
115
|
+
level: level,
|
|
116
|
+
message: message,
|
|
117
|
+
exception: IssueException(
|
|
118
|
+
type: exceptionType,
|
|
119
|
+
mechanism: IssueExceptionMechanism(type: mechanism, handled: handled),
|
|
120
|
+
),
|
|
121
|
+
stackFrames: [
|
|
122
|
+
IssueStackFrame(
|
|
123
|
+
filename: fileID,
|
|
124
|
+
line: line,
|
|
125
|
+
column: column,
|
|
126
|
+
function: function,
|
|
127
|
+
module: fileModule,
|
|
128
|
+
inApp: true,
|
|
129
|
+
),
|
|
130
|
+
],
|
|
131
|
+
breadcrumbs: breadcrumbs,
|
|
132
|
+
breadcrumbsTruncated: breadcrumbsTruncated,
|
|
133
|
+
metadata: metadata,
|
|
134
|
+
context: context,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
final class IssueBreadcrumbStore: @unchecked Sendable {
|
|
140
|
+
private let lock = NSLock()
|
|
141
|
+
private var breadcrumbs: [IssueBreadcrumb] = []
|
|
142
|
+
private var truncated = false
|
|
143
|
+
|
|
144
|
+
func add(_ breadcrumb: IssueBreadcrumb) throws {
|
|
145
|
+
let breadcrumb = try validateIssueBreadcrumb(breadcrumb)
|
|
146
|
+
lock.lock()
|
|
147
|
+
if breadcrumbs.count == maximumIssueBreadcrumbs {
|
|
148
|
+
breadcrumbs.removeFirst()
|
|
149
|
+
truncated = true
|
|
150
|
+
}
|
|
151
|
+
breadcrumbs.append(breadcrumb)
|
|
152
|
+
lock.unlock()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func clear() {
|
|
156
|
+
lock.lock()
|
|
157
|
+
breadcrumbs.removeAll(keepingCapacity: true)
|
|
158
|
+
truncated = false
|
|
159
|
+
lock.unlock()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func snapshot() -> (breadcrumbs: [IssueBreadcrumb], truncated: Bool) {
|
|
163
|
+
lock.lock()
|
|
164
|
+
defer { lock.unlock() }
|
|
165
|
+
return (breadcrumbs, truncated)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let maximumIssueBreadcrumbs = 64
|
|
170
|
+
|
|
171
|
+
func normalizeIssueAttributes(_ value: IssueAttributes) throws -> IssueAttributes {
|
|
172
|
+
let exception = try value.exception.map(validateIssueException)
|
|
173
|
+
let stackFrames = try normalizeIssueStackFrames(value.stackFrames)
|
|
174
|
+
let breadcrumbs = try normalizeIssueBreadcrumbs(value.breadcrumbs)
|
|
175
|
+
let breadcrumbsWereCapped = (value.breadcrumbs?.count ?? 0) > maximumIssueBreadcrumbs
|
|
176
|
+
let context = try value.context.map { try validateTelemetryContext($0) }
|
|
177
|
+
try validateMetadata(value.metadata, label: "issue metadata")
|
|
178
|
+
return IssueAttributes(
|
|
179
|
+
title: value.title,
|
|
180
|
+
level: value.level,
|
|
181
|
+
message: value.message,
|
|
182
|
+
exception: exception,
|
|
183
|
+
stackFrames: stackFrames,
|
|
184
|
+
breadcrumbs: breadcrumbs,
|
|
185
|
+
breadcrumbsTruncated: breadcrumbsWereCapped ? true : value.breadcrumbsTruncated,
|
|
186
|
+
metadata: value.metadata,
|
|
187
|
+
context: context,
|
|
188
|
+
nativeStackFrames: value.nativeStackFrames,
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func issueAttributes(
|
|
193
|
+
_ value: IssueAttributes,
|
|
194
|
+
adding snapshot: (breadcrumbs: [IssueBreadcrumb], truncated: Bool),
|
|
195
|
+
) -> IssueAttributes {
|
|
196
|
+
guard !snapshot.breadcrumbs.isEmpty else {
|
|
197
|
+
return value
|
|
198
|
+
}
|
|
199
|
+
let explicit = value.breadcrumbs ?? []
|
|
200
|
+
let combined = snapshot.breadcrumbs + explicit
|
|
201
|
+
let capped = Array(combined.suffix(maximumIssueBreadcrumbs))
|
|
202
|
+
return IssueAttributes(
|
|
203
|
+
title: value.title,
|
|
204
|
+
level: value.level,
|
|
205
|
+
message: value.message,
|
|
206
|
+
exception: value.exception,
|
|
207
|
+
stackFrames: value.stackFrames,
|
|
208
|
+
breadcrumbs: capped,
|
|
209
|
+
breadcrumbsTruncated: value.breadcrumbsTruncated == true
|
|
210
|
+
|| snapshot.truncated
|
|
211
|
+
|| combined.count > maximumIssueBreadcrumbs,
|
|
212
|
+
metadata: value.metadata,
|
|
213
|
+
context: value.context,
|
|
214
|
+
nativeStackFrames: value.nativeStackFrames,
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func validateIssueException(_ value: IssueException) throws -> IssueException {
|
|
219
|
+
let exceptionType = try issueText(
|
|
220
|
+
value.type,
|
|
221
|
+
label: "issue exception type",
|
|
222
|
+
maximum: 256,
|
|
223
|
+
disallowLocationDelimiters: true,
|
|
224
|
+
)
|
|
225
|
+
let mechanism = try value.mechanism.map { mechanism in
|
|
226
|
+
guard validMachineKey(mechanism.type, maximum: 64, separators: ["_", ".", ":", "-"]) else {
|
|
227
|
+
throw issueValidationError("issue exception mechanism type is invalid")
|
|
228
|
+
}
|
|
229
|
+
return IssueExceptionMechanism(type: mechanism.type, handled: mechanism.handled)
|
|
230
|
+
}
|
|
231
|
+
return IssueException(type: exceptionType, mechanism: mechanism)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func validateIssueBreadcrumb(_ value: IssueBreadcrumb) throws -> IssueBreadcrumb {
|
|
235
|
+
try requireTimestamp(value.timestamp)
|
|
236
|
+
guard validMachineKey(value.category, maximum: 64, separators: ["_", ".", ":", "-"]) else {
|
|
237
|
+
throw issueValidationError("issue breadcrumb category is invalid")
|
|
238
|
+
}
|
|
239
|
+
let type = try value.type.map { rawValue in
|
|
240
|
+
guard validMachineKey(rawValue, maximum: 64, separators: ["_", ".", ":", "-"]) else {
|
|
241
|
+
throw issueValidationError("issue breadcrumb type is invalid")
|
|
242
|
+
}
|
|
243
|
+
return rawValue
|
|
244
|
+
}
|
|
245
|
+
let message = try value.message.map {
|
|
246
|
+
try issueText($0, label: "issue breadcrumb message", maximum: 512)
|
|
247
|
+
}
|
|
248
|
+
try validateBreadcrumbData(value.data)
|
|
249
|
+
return IssueBreadcrumb(
|
|
250
|
+
timestamp: value.timestamp,
|
|
251
|
+
category: value.category,
|
|
252
|
+
type: type,
|
|
253
|
+
level: value.level,
|
|
254
|
+
message: message,
|
|
255
|
+
data: value.data,
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private func normalizeIssueStackFrames(_ values: [IssueStackFrame]?) throws -> [IssueStackFrame]? {
|
|
260
|
+
guard let values else {
|
|
261
|
+
return nil
|
|
262
|
+
}
|
|
263
|
+
guard !values.isEmpty else {
|
|
264
|
+
throw issueValidationError("issue stackFrames must not be empty")
|
|
265
|
+
}
|
|
266
|
+
return try values.prefix(32).map(validateIssueStackFrame)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private func validateIssueStackFrame(_ value: IssueStackFrame) throws -> IssueStackFrame {
|
|
270
|
+
let filename = try sanitizedIssueFilename(value.filename)
|
|
271
|
+
guard (1 ... Int(Int32.max)).contains(value.line), (1 ... Int(Int32.max)).contains(value.column) else {
|
|
272
|
+
throw issueValidationError("issue stack frame line and column must be positive 32-bit integers")
|
|
273
|
+
}
|
|
274
|
+
let function = try value.function.map {
|
|
275
|
+
try issueText($0, label: "issue stack frame function", maximum: 256)
|
|
276
|
+
}
|
|
277
|
+
let module = try value.module.map {
|
|
278
|
+
try issueText(
|
|
279
|
+
$0,
|
|
280
|
+
label: "issue stack frame module",
|
|
281
|
+
maximum: 512,
|
|
282
|
+
disallowLocationDelimiters: true,
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
let debugId = try value.debugId.map { rawValue -> String in
|
|
286
|
+
guard let uuid = UUID(uuidString: rawValue) else {
|
|
287
|
+
throw issueValidationError("issue stack frame debugId must be a UUID")
|
|
288
|
+
}
|
|
289
|
+
return uuid.uuidString.lowercased()
|
|
290
|
+
}
|
|
291
|
+
return IssueStackFrame(
|
|
292
|
+
filename: filename,
|
|
293
|
+
line: value.line,
|
|
294
|
+
column: value.column,
|
|
295
|
+
function: function,
|
|
296
|
+
module: module,
|
|
297
|
+
inApp: value.inApp,
|
|
298
|
+
debugId: debugId,
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func normalizeIssueBreadcrumbs(_ values: [IssueBreadcrumb]?) throws -> [IssueBreadcrumb]? {
|
|
303
|
+
guard let values else {
|
|
304
|
+
return nil
|
|
305
|
+
}
|
|
306
|
+
guard !values.isEmpty else {
|
|
307
|
+
throw issueValidationError("issue breadcrumbs must not be empty")
|
|
308
|
+
}
|
|
309
|
+
return try values.suffix(maximumIssueBreadcrumbs).map(validateIssueBreadcrumb)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private func validateBreadcrumbData(_ data: Metadata?) throws {
|
|
313
|
+
guard let data else {
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
guard data.count <= 8 else {
|
|
317
|
+
throw issueValidationError("issue breadcrumb data must contain at most 8 entries")
|
|
318
|
+
}
|
|
319
|
+
for (key, value) in data {
|
|
320
|
+
guard validMachineKey(key, maximum: 64, separators: ["_", ".", "-"]) else {
|
|
321
|
+
throw issueValidationError("issue breadcrumb data key is invalid")
|
|
322
|
+
}
|
|
323
|
+
switch value {
|
|
324
|
+
case let .string(value):
|
|
325
|
+
_ = try issueText(value, label: "issue breadcrumb data value for \(key)", maximum: 256)
|
|
326
|
+
case let .double(value):
|
|
327
|
+
guard value.isFinite else {
|
|
328
|
+
throw issueValidationError("issue breadcrumb data numeric values must be finite")
|
|
329
|
+
}
|
|
330
|
+
case .int, .bool, .null:
|
|
331
|
+
break
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private func sanitizedIssueFilename(_ value: String) throws -> String {
|
|
337
|
+
var normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
338
|
+
.replacingOccurrences(of: "\\", with: "/")
|
|
339
|
+
if let delimiter = normalized.firstIndex(where: { $0 == "?" || $0 == "#" }) {
|
|
340
|
+
normalized = String(normalized[..<delimiter])
|
|
341
|
+
}
|
|
342
|
+
let isAbsoluteFilePath = normalized.hasPrefix("/")
|
|
343
|
+
|| normalized.lowercased().hasPrefix("file://")
|
|
344
|
+
|| (normalized.count >= 3
|
|
345
|
+
&& normalized[normalized.index(after: normalized.startIndex)] == ":"
|
|
346
|
+
&& normalized[normalized.index(normalized.startIndex, offsetBy: 2)] == "/")
|
|
347
|
+
if isAbsoluteFilePath, let last = normalized.split(separator: "/").last {
|
|
348
|
+
normalized = String(last)
|
|
349
|
+
}
|
|
350
|
+
return try issueText(
|
|
351
|
+
normalized,
|
|
352
|
+
label: "issue stack frame filename",
|
|
353
|
+
maximum: 2048,
|
|
354
|
+
disallowLocationDelimiters: true,
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private func issueText(
|
|
359
|
+
_ value: String,
|
|
360
|
+
label: String,
|
|
361
|
+
maximum: Int,
|
|
362
|
+
disallowLocationDelimiters: Bool = false,
|
|
363
|
+
) throws -> String {
|
|
364
|
+
let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
365
|
+
guard !normalized.isEmpty, normalized.count <= maximum, !containsForbiddenControl(normalized),
|
|
366
|
+
!disallowLocationDelimiters || (!normalized.contains("?") && !normalized.contains("#"))
|
|
367
|
+
else {
|
|
368
|
+
throw issueValidationError("\(label) is invalid")
|
|
369
|
+
}
|
|
370
|
+
return normalized
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private func safeSwiftErrorType(_ error: any Error) -> String {
|
|
374
|
+
let reflected = String(reflecting: Swift.type(of: error))
|
|
375
|
+
let normalized = reflected.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
376
|
+
guard !normalized.isEmpty, !containsForbiddenControl(normalized) else {
|
|
377
|
+
return "Swift.Error"
|
|
378
|
+
}
|
|
379
|
+
let filtered = normalized.filter { $0 != "?" && $0 != "#" }
|
|
380
|
+
return String(filtered.prefix(256))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private func issueValidationError(_ message: String) -> SdkError {
|
|
384
|
+
SdkError(code: "validation_error", message: message)
|
|
385
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
public final class LogBrewLifecycleTracker {
|
|
6
|
+
private let client: LogBrewClient
|
|
7
|
+
private let eventIDPrefix: String
|
|
8
|
+
private let context: Metadata?
|
|
9
|
+
private let lock = NSLock()
|
|
10
|
+
private var currentState: String
|
|
11
|
+
private var currentStartedAtMs: Double
|
|
12
|
+
private var nextEventSequence = 1
|
|
13
|
+
|
|
14
|
+
public init(
|
|
15
|
+
client: LogBrewClient,
|
|
16
|
+
initialState: String,
|
|
17
|
+
initialTimestampMs: Double,
|
|
18
|
+
eventIDPrefix: String = "evt_lifecycle",
|
|
19
|
+
context: Metadata? = nil,
|
|
20
|
+
) throws {
|
|
21
|
+
self.client = client
|
|
22
|
+
currentState = try normalizedLifecycleState("lifecycle initialState", initialState)
|
|
23
|
+
currentStartedAtMs = try validatedLifecycleTimestampMs("lifecycle initialTimestampMs", initialTimestampMs)
|
|
24
|
+
self.eventIDPrefix = try normalizedLifecycleEventIDPrefix(eventIDPrefix)
|
|
25
|
+
self.context = context
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@discardableResult
|
|
29
|
+
public func captureTransition(
|
|
30
|
+
to nextState: String,
|
|
31
|
+
timestamp: String,
|
|
32
|
+
atMs: Double,
|
|
33
|
+
metadata: Metadata? = nil,
|
|
34
|
+
) throws -> Bool {
|
|
35
|
+
let normalizedNextState = try normalizedLifecycleState("lifecycle nextState", nextState)
|
|
36
|
+
let checkedAtMs = try validatedLifecycleTimestampMs("lifecycle atMs", atMs)
|
|
37
|
+
|
|
38
|
+
lock.lock()
|
|
39
|
+
defer {
|
|
40
|
+
lock.unlock()
|
|
41
|
+
}
|
|
42
|
+
if normalizedNextState == currentState {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
if checkedAtMs < currentStartedAtMs {
|
|
46
|
+
throw SdkError(
|
|
47
|
+
code: "validation_error",
|
|
48
|
+
message: "lifecycle atMs must be greater than or equal to the current state start",
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
let eventID = "\(eventIDPrefix)_\(nextEventSequence)"
|
|
52
|
+
let previousState = currentState
|
|
53
|
+
let durationMs = checkedAtMs - currentStartedAtMs
|
|
54
|
+
try client.captureLifecycleSpan(
|
|
55
|
+
eventID,
|
|
56
|
+
timestamp: timestamp,
|
|
57
|
+
previousState: previousState,
|
|
58
|
+
currentState: normalizedNextState,
|
|
59
|
+
durationMs: durationMs,
|
|
60
|
+
context: context,
|
|
61
|
+
metadata: metadata,
|
|
62
|
+
)
|
|
63
|
+
nextEventSequence += 1
|
|
64
|
+
currentState = normalizedNextState
|
|
65
|
+
currentStartedAtMs = checkedAtMs
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public extension LogBrewClient {
|
|
71
|
+
func captureLifecycleSpan(
|
|
72
|
+
_ id: String,
|
|
73
|
+
timestamp: String,
|
|
74
|
+
previousState: String,
|
|
75
|
+
currentState: String,
|
|
76
|
+
durationMs: Double? = nil,
|
|
77
|
+
context: Metadata? = nil,
|
|
78
|
+
metadata: Metadata? = nil,
|
|
79
|
+
) throws {
|
|
80
|
+
let normalizedPreviousState = try normalizedLifecycleState("lifecycle previousState", previousState)
|
|
81
|
+
let normalizedCurrentState = try normalizedLifecycleState("lifecycle currentState", currentState)
|
|
82
|
+
let checkedDurationMs = try validatedLifecycleDurationMs(durationMs)
|
|
83
|
+
let sourceContext = LogBrewTrace.current
|
|
84
|
+
let spanContext = sourceContext.map(LogBrewTraceContext.child(of:)) ?? LogBrewTraceContext.fallbackRoot()
|
|
85
|
+
|
|
86
|
+
var spanMetadata: Metadata = [:]
|
|
87
|
+
try copyMetadata(context, into: &spanMetadata)
|
|
88
|
+
try copyMetadata(metadata, into: &spanMetadata)
|
|
89
|
+
spanMetadata["source"] = .string("swift.lifecycle")
|
|
90
|
+
spanMetadata["previousState"] = .string(normalizedPreviousState)
|
|
91
|
+
spanMetadata["currentState"] = .string(normalizedCurrentState)
|
|
92
|
+
if checkedDurationMs != nil {
|
|
93
|
+
spanMetadata["durationSource"] = .string("previous_state")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try span(
|
|
97
|
+
id,
|
|
98
|
+
timestamp: timestamp,
|
|
99
|
+
attributes: SpanAttributes(
|
|
100
|
+
name: "swift.lifecycle:\(normalizedPreviousState)->\(normalizedCurrentState)",
|
|
101
|
+
traceId: spanContext.traceId,
|
|
102
|
+
spanId: spanContext.spanId,
|
|
103
|
+
parentSpanId: spanContext.parentSpanId,
|
|
104
|
+
status: .ok,
|
|
105
|
+
durationMs: checkedDurationMs,
|
|
106
|
+
metadata: LogBrewTrace.mergeTraceMetadata(spanMetadata, context: spanContext),
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func normalizedLifecycleState(_ label: String, _ state: String) throws -> String {
|
|
113
|
+
try requireNonEmpty(label, state)
|
|
114
|
+
return state.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private func validatedLifecycleDurationMs(_ durationMs: Double?) throws -> Double? {
|
|
118
|
+
guard let durationMs else {
|
|
119
|
+
return nil
|
|
120
|
+
}
|
|
121
|
+
guard durationMs.isFinite, durationMs >= 0 else {
|
|
122
|
+
throw SdkError(code: "validation_error", message: "lifecycle durationMs must be finite and non-negative")
|
|
123
|
+
}
|
|
124
|
+
return durationMs
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private func validatedLifecycleTimestampMs(_ label: String, _ timestampMs: Double) throws -> Double {
|
|
128
|
+
guard timestampMs.isFinite, timestampMs >= 0 else {
|
|
129
|
+
throw SdkError(code: "validation_error", message: "\(label) must be finite and non-negative")
|
|
130
|
+
}
|
|
131
|
+
return timestampMs
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func normalizedLifecycleEventIDPrefix(_ prefix: String) throws -> String {
|
|
135
|
+
let normalized = prefix.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
136
|
+
try requireNonEmpty("lifecycle eventIDPrefix", normalized)
|
|
137
|
+
return normalized
|
|
138
|
+
}
|