@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,277 @@
|
|
|
1
|
+
// Generated by scripts/sync-apple-native-sources.mjs.
|
|
2
|
+
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
|
+
import Foundation
|
|
4
|
+
#if canImport(FoundationNetworking)
|
|
5
|
+
import FoundationNetworking
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
public struct LogBrewURLSessionSpan {
|
|
9
|
+
public let request: URLRequest
|
|
10
|
+
public let traceContext: LogBrewTraceContext
|
|
11
|
+
public let method: String
|
|
12
|
+
public let routeTemplate: String
|
|
13
|
+
|
|
14
|
+
public init(
|
|
15
|
+
request: URLRequest,
|
|
16
|
+
traceContext: LogBrewTraceContext,
|
|
17
|
+
method: String,
|
|
18
|
+
routeTemplate: String,
|
|
19
|
+
) {
|
|
20
|
+
self.request = request
|
|
21
|
+
self.traceContext = traceContext
|
|
22
|
+
self.method = method
|
|
23
|
+
self.routeTemplate = routeTemplate
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public struct LogBrewURLSessionTimings: Equatable, Sendable {
|
|
28
|
+
private let values: Metadata
|
|
29
|
+
|
|
30
|
+
public init(
|
|
31
|
+
fetchMs: Double? = nil,
|
|
32
|
+
redirectMs: Double? = nil,
|
|
33
|
+
nameLookupMs: Double? = nil,
|
|
34
|
+
connectMs: Double? = nil,
|
|
35
|
+
tlsMs: Double? = nil,
|
|
36
|
+
sendMs: Double? = nil,
|
|
37
|
+
waitMs: Double? = nil,
|
|
38
|
+
receiveMs: Double? = nil,
|
|
39
|
+
requestBodyBytes: Int64? = nil,
|
|
40
|
+
responseBodyBytes: Int64? = nil,
|
|
41
|
+
) throws {
|
|
42
|
+
var metadata: Metadata = [:]
|
|
43
|
+
try Self.addDuration(fetchMs, key: "requestFetchMs", into: &metadata)
|
|
44
|
+
try Self.addDuration(redirectMs, key: "requestRedirectMs", into: &metadata)
|
|
45
|
+
try Self.addDuration(nameLookupMs, key: "requestNameLookupMs", into: &metadata)
|
|
46
|
+
try Self.addDuration(connectMs, key: "requestConnectMs", into: &metadata)
|
|
47
|
+
try Self.addDuration(tlsMs, key: "requestTlsMs", into: &metadata)
|
|
48
|
+
try Self.addDuration(sendMs, key: "requestSendMs", into: &metadata)
|
|
49
|
+
try Self.addDuration(waitMs, key: "requestWaitMs", into: &metadata)
|
|
50
|
+
try Self.addDuration(receiveMs, key: "requestReceiveMs", into: &metadata)
|
|
51
|
+
try Self.addByteCount(requestBodyBytes, key: "requestBodyBytes", into: &metadata)
|
|
52
|
+
try Self.addByteCount(responseBodyBytes, key: "responseBodyBytes", into: &metadata)
|
|
53
|
+
values = metadata
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public init(taskMetrics: URLSessionTaskMetrics) throws {
|
|
57
|
+
var metadata: Metadata = [:]
|
|
58
|
+
try Self.addDuration(taskMetrics.taskInterval, key: "requestFetchMs", into: &metadata)
|
|
59
|
+
|
|
60
|
+
let networkTransactions = taskMetrics.transactionMetrics.filter { $0.resourceFetchType != .localCache }
|
|
61
|
+
let mainTransaction = networkTransactions.last
|
|
62
|
+
try Self.addRedirectTimings(networkTransactions.dropLast(), into: &metadata)
|
|
63
|
+
if let mainTransaction {
|
|
64
|
+
try Self.addMainTransactionTimings(mainTransaction, into: &metadata)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
values = metadata
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var metadata: Metadata {
|
|
71
|
+
values
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private static func addRedirectTimings(
|
|
75
|
+
_ transactions: ArraySlice<URLSessionTaskTransactionMetrics>,
|
|
76
|
+
into metadata: inout Metadata,
|
|
77
|
+
) throws {
|
|
78
|
+
let redirectStarts = transactions.compactMap(\.fetchStartDate)
|
|
79
|
+
let redirectEnds = transactions.compactMap(\.responseEndDate)
|
|
80
|
+
if let firstRedirectStart = redirectStarts.first, let lastRedirectEnd = redirectEnds.last {
|
|
81
|
+
try addDuration(firstRedirectStart, lastRedirectEnd, key: "requestRedirectMs", into: &metadata)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private static func addMainTransactionTimings(
|
|
86
|
+
_ transaction: URLSessionTaskTransactionMetrics,
|
|
87
|
+
into metadata: inout Metadata,
|
|
88
|
+
) throws {
|
|
89
|
+
try addDuration(
|
|
90
|
+
transaction.domainLookupStartDate,
|
|
91
|
+
transaction.domainLookupEndDate,
|
|
92
|
+
key: "requestNameLookupMs",
|
|
93
|
+
into: &metadata,
|
|
94
|
+
)
|
|
95
|
+
try addDuration(
|
|
96
|
+
transaction.connectStartDate,
|
|
97
|
+
transaction.connectEndDate,
|
|
98
|
+
key: "requestConnectMs",
|
|
99
|
+
into: &metadata,
|
|
100
|
+
)
|
|
101
|
+
try addDuration(
|
|
102
|
+
transaction.secureConnectionStartDate,
|
|
103
|
+
transaction.secureConnectionEndDate,
|
|
104
|
+
key: "requestTlsMs",
|
|
105
|
+
into: &metadata,
|
|
106
|
+
)
|
|
107
|
+
try addDuration(
|
|
108
|
+
transaction.requestStartDate,
|
|
109
|
+
transaction.requestEndDate,
|
|
110
|
+
key: "requestSendMs",
|
|
111
|
+
into: &metadata,
|
|
112
|
+
)
|
|
113
|
+
try addDuration(
|
|
114
|
+
transaction.requestEndDate ?? transaction.requestStartDate,
|
|
115
|
+
transaction.responseStartDate,
|
|
116
|
+
key: "requestWaitMs",
|
|
117
|
+
into: &metadata,
|
|
118
|
+
)
|
|
119
|
+
try addDuration(
|
|
120
|
+
transaction.responseStartDate,
|
|
121
|
+
transaction.responseEndDate,
|
|
122
|
+
key: "requestReceiveMs",
|
|
123
|
+
into: &metadata,
|
|
124
|
+
)
|
|
125
|
+
try addByteCount(
|
|
126
|
+
transaction.countOfRequestBodyBytesSent,
|
|
127
|
+
key: "requestBodyBytes",
|
|
128
|
+
into: &metadata,
|
|
129
|
+
)
|
|
130
|
+
try addByteCount(
|
|
131
|
+
transaction.countOfResponseBodyBytesReceived,
|
|
132
|
+
key: "responseBodyBytes",
|
|
133
|
+
into: &metadata,
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static func addDuration(_ value: Double?, key: String, into metadata: inout Metadata) throws {
|
|
138
|
+
guard let value else {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
guard value >= 0, value.isFinite else {
|
|
142
|
+
throw SdkError(
|
|
143
|
+
code: "validation_error",
|
|
144
|
+
message: "URLSession timing values must be non-negative and finite",
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
metadata[key] = .double(value)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static func addDuration(_ interval: DateInterval, key: String, into metadata: inout Metadata) throws {
|
|
151
|
+
try addDuration(interval.duration * 1000, key: key, into: &metadata)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private static func addDuration(_ start: Date?, _ end: Date?, key: String, into metadata: inout Metadata) throws {
|
|
155
|
+
guard let start, let end else {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
try addDuration(end.timeIntervalSince(start) * 1000, key: key, into: &metadata)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private static func addByteCount(_ value: Int64?, key: String, into metadata: inout Metadata) throws {
|
|
162
|
+
guard let value else {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
guard value >= 0, value <= Int64(Int.max) else {
|
|
166
|
+
throw SdkError(code: "validation_error", message: "URLSession byte counts must be non-negative")
|
|
167
|
+
}
|
|
168
|
+
metadata[key] = .int(Int(value))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public extension LogBrewTrace {
|
|
173
|
+
static func startURLSessionSpan(
|
|
174
|
+
for request: URLRequest,
|
|
175
|
+
routeTemplate: String? = nil,
|
|
176
|
+
context: LogBrewTraceContext? = current,
|
|
177
|
+
) throws -> LogBrewURLSessionSpan {
|
|
178
|
+
let method = try normalizedNetworkMethod(request.httpMethod ?? "GET")
|
|
179
|
+
let route = try routeTemplateFromURLRequest(request, routeTemplate: routeTemplate)
|
|
180
|
+
let spanContext = context.map(LogBrewTraceContext.child(of:)) ?? LogBrewTraceContext.fallbackRoot()
|
|
181
|
+
|
|
182
|
+
var tracedRequest = request
|
|
183
|
+
tracedRequest.setValue(spanContext.traceparent, forHTTPHeaderField: "traceparent")
|
|
184
|
+
|
|
185
|
+
return LogBrewURLSessionSpan(
|
|
186
|
+
request: tracedRequest,
|
|
187
|
+
traceContext: spanContext,
|
|
188
|
+
method: method,
|
|
189
|
+
routeTemplate: route,
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public extension LogBrewClient {
|
|
195
|
+
func captureURLSessionSpan(
|
|
196
|
+
_ id: String,
|
|
197
|
+
timestamp: String,
|
|
198
|
+
span: LogBrewURLSessionSpan,
|
|
199
|
+
statusCode: Int? = nil,
|
|
200
|
+
durationMs: Double? = nil,
|
|
201
|
+
error: Error? = nil,
|
|
202
|
+
metadata: Metadata? = nil,
|
|
203
|
+
timings: LogBrewURLSessionTimings? = nil,
|
|
204
|
+
) throws {
|
|
205
|
+
let checkedStatusCode = try validatedStatusCode(statusCode)
|
|
206
|
+
let checkedDurationMs = try validatedDurationMs(durationMs)
|
|
207
|
+
var spanMetadata: Metadata = [:]
|
|
208
|
+
try copyMetadata(metadata, into: &spanMetadata)
|
|
209
|
+
for key in urlSessionGeneratedMetadataKeys {
|
|
210
|
+
spanMetadata.removeValue(forKey: key)
|
|
211
|
+
}
|
|
212
|
+
spanMetadata["source"] = .string("swift.urlsession")
|
|
213
|
+
spanMetadata["method"] = .string(span.method)
|
|
214
|
+
spanMetadata["routeTemplate"] = .string(span.routeTemplate)
|
|
215
|
+
if let checkedStatusCode {
|
|
216
|
+
spanMetadata["statusCode"] = .int(checkedStatusCode)
|
|
217
|
+
}
|
|
218
|
+
if let error {
|
|
219
|
+
spanMetadata["errorType"] = .string(String(describing: type(of: error)))
|
|
220
|
+
}
|
|
221
|
+
for (key, value) in timings?.metadata ?? [:] {
|
|
222
|
+
spanMetadata[key] = value
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try self.span(
|
|
226
|
+
id,
|
|
227
|
+
timestamp: timestamp,
|
|
228
|
+
attributes: SpanAttributes(
|
|
229
|
+
name: "\(span.method) \(span.routeTemplate)",
|
|
230
|
+
traceId: span.traceContext.traceId,
|
|
231
|
+
spanId: span.traceContext.spanId,
|
|
232
|
+
parentSpanId: span.traceContext.parentSpanId,
|
|
233
|
+
status: urlSessionSpanStatus(statusCode: checkedStatusCode, error: error),
|
|
234
|
+
durationMs: checkedDurationMs,
|
|
235
|
+
metadata: LogBrewTrace.mergeTraceMetadata(spanMetadata, context: span.traceContext),
|
|
236
|
+
),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private let urlSessionGeneratedMetadataKeys: Set<String> = [
|
|
242
|
+
"source",
|
|
243
|
+
"method",
|
|
244
|
+
"routeTemplate",
|
|
245
|
+
"statusCode",
|
|
246
|
+
"errorType",
|
|
247
|
+
"requestFetchMs",
|
|
248
|
+
"requestRedirectMs",
|
|
249
|
+
"requestNameLookupMs",
|
|
250
|
+
"requestConnectMs",
|
|
251
|
+
"requestTlsMs",
|
|
252
|
+
"requestSendMs",
|
|
253
|
+
"requestWaitMs",
|
|
254
|
+
"requestReceiveMs",
|
|
255
|
+
"requestBodyBytes",
|
|
256
|
+
"responseBodyBytes",
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
private func routeTemplateFromURLRequest(_ request: URLRequest, routeTemplate: String?) throws -> String {
|
|
260
|
+
if let routeTemplate {
|
|
261
|
+
return try normalizedRouteTemplate(routeTemplate)
|
|
262
|
+
}
|
|
263
|
+
guard let url = request.url else {
|
|
264
|
+
throw SdkError(code: "validation_error", message: "URLSession request URL is required")
|
|
265
|
+
}
|
|
266
|
+
return try normalizedRouteTemplate(url.absoluteString)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private func urlSessionSpanStatus(statusCode: Int?, error: Error?) -> SpanStatus {
|
|
270
|
+
if error != nil {
|
|
271
|
+
return .error
|
|
272
|
+
}
|
|
273
|
+
guard let statusCode else {
|
|
274
|
+
return .ok
|
|
275
|
+
}
|
|
276
|
+
return statusCode >= 400 ? .error : .ok
|
|
277
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Generated by scripts/sync-apple-native-sources.mjs.
|
|
2
|
+
// Edit the canonical Swift source, then regenerate this package boundary.
|
|
3
|
+
import Foundation
|
|
4
|
+
#if canImport(FoundationNetworking)
|
|
5
|
+
import FoundationNetworking
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
public final class LogBrewURLSessionTracer: @unchecked Sendable {
|
|
9
|
+
private let client: LogBrewClient
|
|
10
|
+
private let dataLoader: @Sendable (URLRequest) async throws -> (Data, URLResponse)
|
|
11
|
+
private let timestampProvider: @Sendable () -> String
|
|
12
|
+
private let nowMsProvider: @Sendable () -> Double
|
|
13
|
+
private let onCaptureError: (@Sendable (any Error) -> Void)?
|
|
14
|
+
private let eventIDPrefix: String
|
|
15
|
+
private let lock = NSLock()
|
|
16
|
+
private var nextEventSequence = 1
|
|
17
|
+
|
|
18
|
+
public convenience init(
|
|
19
|
+
client: LogBrewClient,
|
|
20
|
+
session: URLSession = .shared,
|
|
21
|
+
eventIDPrefix: String = "evt_urlsession",
|
|
22
|
+
timestampProvider: (@Sendable () -> String)? = nil,
|
|
23
|
+
nowMsProvider: (@Sendable () -> Double)? = nil,
|
|
24
|
+
onCaptureError: (@Sendable (any Error) -> Void)? = nil,
|
|
25
|
+
) throws {
|
|
26
|
+
let loader = LogBrewURLSessionDataLoader(session: session)
|
|
27
|
+
try self.init(
|
|
28
|
+
client: client,
|
|
29
|
+
eventIDPrefix: eventIDPrefix,
|
|
30
|
+
timestampProvider: timestampProvider,
|
|
31
|
+
nowMsProvider: nowMsProvider,
|
|
32
|
+
onCaptureError: onCaptureError,
|
|
33
|
+
dataLoader: { request in
|
|
34
|
+
try await loader.data(for: request)
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
init(
|
|
40
|
+
client: LogBrewClient,
|
|
41
|
+
eventIDPrefix: String = "evt_urlsession",
|
|
42
|
+
timestampProvider: (@Sendable () -> String)? = nil,
|
|
43
|
+
nowMsProvider: (@Sendable () -> Double)? = nil,
|
|
44
|
+
onCaptureError: (@Sendable (any Error) -> Void)? = nil,
|
|
45
|
+
dataLoader: @escaping @Sendable (URLRequest) async throws -> (Data, URLResponse),
|
|
46
|
+
) throws {
|
|
47
|
+
let normalizedPrefix = eventIDPrefix.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
48
|
+
try requireNonEmpty("URLSession tracer eventIDPrefix", normalizedPrefix)
|
|
49
|
+
self.client = client
|
|
50
|
+
self.eventIDPrefix = normalizedPrefix
|
|
51
|
+
self.timestampProvider = timestampProvider ?? Self.defaultTimestamp
|
|
52
|
+
self.nowMsProvider = nowMsProvider ?? Self.defaultNowMs
|
|
53
|
+
self.onCaptureError = onCaptureError
|
|
54
|
+
self.dataLoader = dataLoader
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@discardableResult
|
|
58
|
+
public func data(
|
|
59
|
+
for request: URLRequest,
|
|
60
|
+
routeTemplate: String? = nil,
|
|
61
|
+
eventID: String? = nil,
|
|
62
|
+
metadata: Metadata? = nil,
|
|
63
|
+
) async throws -> (Data, URLResponse) {
|
|
64
|
+
let span = try LogBrewTrace.startURLSessionSpan(for: request, routeTemplate: routeTemplate)
|
|
65
|
+
let startedAtMs = nowMsProvider()
|
|
66
|
+
|
|
67
|
+
do {
|
|
68
|
+
let (data, response) = try await dataLoader(span.request)
|
|
69
|
+
captureSpan(URLSessionTraceCapture(
|
|
70
|
+
eventID: eventID,
|
|
71
|
+
span: span,
|
|
72
|
+
response: response,
|
|
73
|
+
durationMs: durationSince(startedAtMs),
|
|
74
|
+
error: nil,
|
|
75
|
+
metadata: metadata,
|
|
76
|
+
))
|
|
77
|
+
return (data, response)
|
|
78
|
+
} catch {
|
|
79
|
+
captureSpan(URLSessionTraceCapture(
|
|
80
|
+
eventID: eventID,
|
|
81
|
+
span: span,
|
|
82
|
+
response: nil,
|
|
83
|
+
durationMs: durationSince(startedAtMs),
|
|
84
|
+
error: error,
|
|
85
|
+
metadata: metadata,
|
|
86
|
+
))
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func captureSpan(_ capture: URLSessionTraceCapture) {
|
|
92
|
+
do {
|
|
93
|
+
try client.captureURLSessionSpan(
|
|
94
|
+
capture.eventID ?? nextEventID(),
|
|
95
|
+
timestamp: timestampProvider(),
|
|
96
|
+
span: capture.span,
|
|
97
|
+
statusCode: (capture.response as? HTTPURLResponse)?.statusCode,
|
|
98
|
+
durationMs: capture.durationMs,
|
|
99
|
+
error: capture.error,
|
|
100
|
+
metadata: capture.metadata,
|
|
101
|
+
)
|
|
102
|
+
} catch {
|
|
103
|
+
onCaptureError?(error)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private func durationSince(_ startedAtMs: Double) -> Double {
|
|
108
|
+
let endedAtMs = nowMsProvider()
|
|
109
|
+
guard startedAtMs.isFinite, endedAtMs.isFinite else {
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
return max(0, endedAtMs - startedAtMs)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private func nextEventID() -> String {
|
|
116
|
+
lock.lock()
|
|
117
|
+
defer {
|
|
118
|
+
lock.unlock()
|
|
119
|
+
}
|
|
120
|
+
let eventID = "\(eventIDPrefix)_\(nextEventSequence)"
|
|
121
|
+
nextEventSequence += 1
|
|
122
|
+
return eventID
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private static func defaultTimestamp() -> String {
|
|
126
|
+
let formatter = ISO8601DateFormatter()
|
|
127
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
128
|
+
return formatter.string(from: Date())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private static func defaultNowMs() -> Double {
|
|
132
|
+
ProcessInfo.processInfo.systemUptime * 1000
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private struct URLSessionTraceCapture {
|
|
137
|
+
let eventID: String?
|
|
138
|
+
let span: LogBrewURLSessionSpan
|
|
139
|
+
let response: URLResponse?
|
|
140
|
+
let durationMs: Double
|
|
141
|
+
let error: (any Error)?
|
|
142
|
+
let metadata: Metadata?
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private struct LogBrewURLSessionDataLoader: @unchecked Sendable {
|
|
146
|
+
let session: URLSession
|
|
147
|
+
|
|
148
|
+
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
|
149
|
+
try await session.data(for: request)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
func validateRelease(_ attributes: ReleaseAttributes) throws -> ReleaseAttributes {
|
|
6
|
+
try requireNonEmpty("release version", attributes.version)
|
|
7
|
+
if let commit = attributes.commit {
|
|
8
|
+
try requireNonEmpty("release commit", commit)
|
|
9
|
+
}
|
|
10
|
+
try validateMetadata(attributes.metadata, label: "release metadata")
|
|
11
|
+
return try ReleaseAttributes(
|
|
12
|
+
version: attributes.version,
|
|
13
|
+
commit: attributes.commit,
|
|
14
|
+
notes: attributes.notes,
|
|
15
|
+
metadata: attributes.metadata,
|
|
16
|
+
context: attributes.context.map { try validateTelemetryContext($0) },
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func validateEnvironment(_ attributes: EnvironmentAttributes) throws -> EnvironmentAttributes {
|
|
21
|
+
try requireNonEmpty("environment name", attributes.name)
|
|
22
|
+
try validateMetadata(attributes.metadata, label: "environment metadata")
|
|
23
|
+
return try EnvironmentAttributes(
|
|
24
|
+
name: attributes.name,
|
|
25
|
+
region: attributes.region,
|
|
26
|
+
metadata: attributes.metadata,
|
|
27
|
+
context: attributes.context.map { try validateTelemetryContext($0) },
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func validateIssue(_ attributes: IssueAttributes) throws -> IssueAttributes {
|
|
32
|
+
try requireNonEmpty("issue title", attributes.title)
|
|
33
|
+
if let frames = attributes.nativeStackFrames {
|
|
34
|
+
guard (1 ... 32).contains(frames.count), frames.allSatisfy(isValidNativeStackFrame) else {
|
|
35
|
+
throw SdkError(code: "validation_error", message: "issue nativeStackFrames must be canonical")
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return try normalizeIssueAttributes(attributes)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private func isValidNativeStackFrame(_ frame: NativeStackFrame) -> Bool {
|
|
42
|
+
UUID(uuidString: frame.imageUuid)?.uuidString.lowercased() == frame.imageUuid
|
|
43
|
+
&& frame.instructionOffset.count == 16
|
|
44
|
+
&& frame.instructionOffset.allSatisfy { ("0" ... "9").contains($0) || ("a" ... "f").contains($0) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func validateLog(_ attributes: LogAttributes) throws -> LogAttributes {
|
|
48
|
+
try requireNonEmpty("log message", attributes.message)
|
|
49
|
+
try validateMetadata(attributes.metadata, label: "log metadata")
|
|
50
|
+
return try LogAttributes(
|
|
51
|
+
message: attributes.message,
|
|
52
|
+
level: attributes.level,
|
|
53
|
+
logger: attributes.logger,
|
|
54
|
+
metadata: attributes.metadata,
|
|
55
|
+
context: attributes.context.map { try validateTelemetryContext($0) },
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func validateSpan(_ attributes: SpanAttributes) throws -> SpanAttributes {
|
|
60
|
+
try requireNonEmpty("span name", attributes.name)
|
|
61
|
+
try requireNonEmpty("span traceId", attributes.traceId)
|
|
62
|
+
try requireNonEmpty("span spanId", attributes.spanId)
|
|
63
|
+
if let parentSpanId = attributes.parentSpanId {
|
|
64
|
+
try requireNonEmpty("span parentSpanId", parentSpanId)
|
|
65
|
+
}
|
|
66
|
+
if let durationMs = attributes.durationMs, durationMs < 0 {
|
|
67
|
+
throw SdkError(code: "validation_error", message: "span durationMs must be non-negative")
|
|
68
|
+
}
|
|
69
|
+
try validateMetadata(attributes.metadata, label: "span metadata")
|
|
70
|
+
return try SpanAttributes(
|
|
71
|
+
name: attributes.name,
|
|
72
|
+
traceId: attributes.traceId,
|
|
73
|
+
spanId: attributes.spanId,
|
|
74
|
+
parentSpanId: attributes.parentSpanId,
|
|
75
|
+
status: attributes.status,
|
|
76
|
+
durationMs: attributes.durationMs,
|
|
77
|
+
events: validateSpanEvents(attributes.events),
|
|
78
|
+
links: validateSpanLinks(attributes.links),
|
|
79
|
+
metadata: attributes.metadata,
|
|
80
|
+
context: attributes.context.map { try validateTelemetryContext($0) },
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func validateAction(_ attributes: ActionAttributes) throws -> ActionAttributes {
|
|
85
|
+
try requireNonEmpty("action name", attributes.name)
|
|
86
|
+
try validateMetadata(attributes.metadata, label: "action metadata")
|
|
87
|
+
return try ActionAttributes(
|
|
88
|
+
name: attributes.name,
|
|
89
|
+
status: attributes.status,
|
|
90
|
+
metadata: attributes.metadata,
|
|
91
|
+
context: attributes.context.map { try validateTelemetryContext($0) },
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
func validateMetric(_ attributes: MetricAttributes) throws -> MetricAttributes {
|
|
96
|
+
try requireNonEmpty("metric name", attributes.name)
|
|
97
|
+
try requireNonEmpty("metric unit", attributes.unit)
|
|
98
|
+
guard attributes.value.isFinite else {
|
|
99
|
+
throw SdkError(code: "validation_error", message: "metric value must be finite")
|
|
100
|
+
}
|
|
101
|
+
switch attributes.kind {
|
|
102
|
+
case .counter, .histogram:
|
|
103
|
+
if attributes.value < 0 {
|
|
104
|
+
throw SdkError(
|
|
105
|
+
code: "validation_error",
|
|
106
|
+
message: "\(attributes.kind.rawValue) metric value must be non-negative",
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
guard attributes.temporality == .delta || attributes.temporality == .cumulative else {
|
|
110
|
+
throw SdkError(
|
|
111
|
+
code: "validation_error",
|
|
112
|
+
message: "\(attributes.kind.rawValue) metric temporality must be cumulative or delta",
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
case .gauge:
|
|
116
|
+
guard attributes.temporality == .instant else {
|
|
117
|
+
throw SdkError(
|
|
118
|
+
code: "validation_error",
|
|
119
|
+
message: "gauge metric temporality must be instant",
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
try validateMetadata(attributes.metadata, label: "metric metadata")
|
|
124
|
+
return try MetricAttributes(
|
|
125
|
+
name: attributes.name,
|
|
126
|
+
kind: attributes.kind,
|
|
127
|
+
value: attributes.value,
|
|
128
|
+
unit: attributes.unit,
|
|
129
|
+
temporality: attributes.temporality,
|
|
130
|
+
metadata: attributes.metadata,
|
|
131
|
+
context: attributes.context.map { try validateTelemetryContext($0) },
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func requireNonEmpty(_ label: String, _ value: String) throws {
|
|
136
|
+
if value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
137
|
+
throw SdkError(code: "validation_error", message: "\(label) must be non-empty")
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func requireTimestamp(_ timestamp: String) throws {
|
|
142
|
+
try requireNonEmpty("timestamp", timestamp)
|
|
143
|
+
let timePart = timestamp.split(separator: "T", maxSplits: 1).dropFirst().first
|
|
144
|
+
let hasZuluSuffix = timestamp.hasSuffix("Z")
|
|
145
|
+
let hasPositiveOffset = timePart?.contains("+") == true
|
|
146
|
+
let hasNegativeOffset = timePart?.dropFirst().contains("-") == true
|
|
147
|
+
let hasTimeZone = hasZuluSuffix || hasPositiveOffset || hasNegativeOffset
|
|
148
|
+
guard hasTimeZone else {
|
|
149
|
+
throw SdkError(
|
|
150
|
+
code: "validation_error",
|
|
151
|
+
message: "timestamp must include a timezone offset: \(timestamp)",
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let formatter = ISO8601DateFormatter()
|
|
156
|
+
formatter.formatOptions = [.withInternetDateTime]
|
|
157
|
+
if formatter.date(from: timestamp) != nil {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
162
|
+
if formatter.date(from: timestamp) == nil {
|
|
163
|
+
throw SdkError(code: "validation_error", message: "invalid timestamp: \(timestamp)")
|
|
164
|
+
}
|
|
165
|
+
}
|