@logbrew/react-native 0.1.11 → 0.1.13

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.
Files changed (61) hide show
  1. package/LogBrewReactNative.podspec +25 -3
  2. package/README.md +106 -3
  3. package/apple-native-diagnostics.d.ts +52 -0
  4. package/apple-native-diagnostics.js +286 -0
  5. package/expo.cjs +46 -0
  6. package/expo.d.ts +16 -0
  7. package/expo.js +5 -0
  8. package/fatal-replay.cjs +12 -1
  9. package/global-errors.cjs +3 -1
  10. package/index.cjs +84 -2
  11. package/index.d.cts +4 -0
  12. package/index.d.ts +4 -0
  13. package/index.js +84 -2
  14. package/index.native.d.ts +4 -0
  15. package/index.native.js +14 -1
  16. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
  17. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
  18. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
  19. package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
  20. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
  21. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
  22. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
  23. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
  29. package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
  30. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
  31. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
  32. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
  33. package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
  34. package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
  35. package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
  36. package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
  37. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
  38. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
  39. package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
  40. package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
  41. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
  42. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
  43. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
  44. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
  45. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
  46. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
  47. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
  48. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
  49. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
  50. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
  51. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
  52. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
  53. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
  54. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
  55. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
  56. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
  57. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
  58. package/package.json +43 -5
  59. package/promise-rejections.cjs +3 -1
  60. package/scripts/sync-apple-native-sources.mjs +93 -0
  61. package/src/NativeLogBrewAppleDiagnostics.ts +12 -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
+ }