@logbrew/react-native 0.1.11 → 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.
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 +42 -4
  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,357 @@
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 struct LogBrewTraceContext: Equatable, Sendable {
6
+ public let traceId: String
7
+ public let spanId: String
8
+ public let parentSpanId: String?
9
+ public let traceFlags: String
10
+ public let sampled: Bool
11
+
12
+ public var traceparent: String {
13
+ "00-\(traceId)-\(spanId)-\(traceFlags)"
14
+ }
15
+
16
+ public init(
17
+ traceId: String,
18
+ spanId: String,
19
+ parentSpanId: String? = nil,
20
+ traceFlags: String = "01",
21
+ ) throws {
22
+ let normalizedTraceId = try Self.normalizedTraceId(traceId)
23
+ let normalizedSpanId = try Self.normalizedSpanId("spanId", spanId)
24
+ let normalizedParentSpanId = try parentSpanId.map { try Self.normalizedSpanId("parentSpanId", $0) }
25
+ let normalizedTraceFlags = try Self.normalizedTraceFlags(traceFlags)
26
+ self.init(
27
+ validatedTraceId: normalizedTraceId,
28
+ spanId: normalizedSpanId,
29
+ parentSpanId: normalizedParentSpanId,
30
+ traceFlags: normalizedTraceFlags,
31
+ )
32
+ }
33
+
34
+ static func createRoot(traceFlags: String = "01") throws -> LogBrewTraceContext {
35
+ let normalizedTraceFlags = try normalizedTraceFlags(traceFlags)
36
+ return LogBrewTraceContext(
37
+ validatedTraceId: randomTraceId(),
38
+ spanId: randomSpanId(),
39
+ parentSpanId: nil,
40
+ traceFlags: normalizedTraceFlags,
41
+ )
42
+ }
43
+
44
+ static func fallbackRoot() -> LogBrewTraceContext {
45
+ LogBrewTraceContext(
46
+ validatedTraceId: randomTraceId(),
47
+ spanId: randomSpanId(),
48
+ parentSpanId: nil,
49
+ traceFlags: "01",
50
+ )
51
+ }
52
+
53
+ static func child(of parent: LogBrewTraceContext) -> LogBrewTraceContext {
54
+ LogBrewTraceContext(
55
+ validatedTraceId: parent.traceId,
56
+ spanId: randomSpanId(),
57
+ parentSpanId: parent.spanId,
58
+ traceFlags: parent.traceFlags,
59
+ )
60
+ }
61
+
62
+ fileprivate static func child(
63
+ traceId: String,
64
+ parentSpanId: String,
65
+ traceFlags: String,
66
+ ) -> LogBrewTraceContext {
67
+ LogBrewTraceContext(
68
+ validatedTraceId: traceId,
69
+ spanId: randomSpanId(),
70
+ parentSpanId: parentSpanId,
71
+ traceFlags: traceFlags,
72
+ )
73
+ }
74
+
75
+ private init(validatedTraceId: String, spanId: String, parentSpanId: String?, traceFlags: String) {
76
+ traceId = validatedTraceId
77
+ self.spanId = spanId
78
+ self.parentSpanId = parentSpanId
79
+ self.traceFlags = traceFlags
80
+ sampled = (Int(traceFlags, radix: 16) ?? 0) & 1 == 1
81
+ }
82
+
83
+ private static func randomTraceId() -> String {
84
+ let value = UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased()
85
+ return value == zeroTraceId ? "00000000000000000000000000000001" : value
86
+ }
87
+
88
+ private static func randomSpanId() -> String {
89
+ let value = String(UUID().uuidString.replacingOccurrences(of: "-", with: "").lowercased().prefix(16))
90
+ return value == zeroSpanId ? "0000000000000001" : value
91
+ }
92
+
93
+ fileprivate static func normalizedTraceId(_ value: String) throws -> String {
94
+ let normalized = try normalizedHex("traceId", value, length: 32)
95
+ if normalized == zeroTraceId {
96
+ throw SdkError(code: "validation_error", message: "traceId must not be all zeros")
97
+ }
98
+ return normalized
99
+ }
100
+
101
+ fileprivate static func normalizedSpanId(_ label: String, _ value: String) throws -> String {
102
+ let normalized = try normalizedHex(label, value, length: 16)
103
+ if normalized == zeroSpanId {
104
+ throw SdkError(code: "validation_error", message: "\(label) must not be all zeros")
105
+ }
106
+ return normalized
107
+ }
108
+
109
+ fileprivate static func normalizedTraceFlags(_ value: String) throws -> String {
110
+ try normalizedHex("traceFlags", value, length: 2)
111
+ }
112
+
113
+ private static func normalizedHex(_ label: String, _ value: String, length: Int) throws -> String {
114
+ let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
115
+ guard normalized.count == length, normalized.allSatisfy(\.isHexDigit) else {
116
+ throw SdkError(
117
+ code: "validation_error",
118
+ message: "\(label) must be \(length) lowercase or uppercase hex characters",
119
+ )
120
+ }
121
+ return normalized
122
+ }
123
+ }
124
+
125
+ public struct LogBrewOpenTelemetrySpanContext: Equatable, Sendable {
126
+ public let traceId: String
127
+ public let spanId: String
128
+ public let traceFlags: String
129
+ public let sampled: Bool
130
+
131
+ public init(
132
+ traceId: String,
133
+ spanId: String,
134
+ traceFlags: String = "01",
135
+ ) throws {
136
+ let normalizedTraceId = try LogBrewTraceContext.normalizedTraceId(traceId)
137
+ let normalizedSpanId = try LogBrewTraceContext.normalizedSpanId("OpenTelemetry spanId", spanId)
138
+ let normalizedTraceFlags = try LogBrewTraceContext.normalizedTraceFlags(traceFlags)
139
+
140
+ self.traceId = normalizedTraceId
141
+ self.spanId = normalizedSpanId
142
+ self.traceFlags = normalizedTraceFlags
143
+ sampled = (Int(normalizedTraceFlags, radix: 16) ?? 0) & 1 == 1
144
+ }
145
+
146
+ public init(
147
+ traceId: String,
148
+ spanId: String,
149
+ sampled: Bool,
150
+ ) throws {
151
+ try self.init(traceId: traceId, spanId: spanId, traceFlags: sampled ? "01" : "00")
152
+ }
153
+ }
154
+
155
+ public protocol LogBrewOpenTelemetrySpanContextCarrier {
156
+ var logBrewOpenTelemetryTraceId: String { get }
157
+ var logBrewOpenTelemetrySpanId: String { get }
158
+ var logBrewOpenTelemetryTraceFlags: String { get }
159
+ var logBrewOpenTelemetryIsValid: Bool { get }
160
+ }
161
+
162
+ public enum LogBrewTrace {
163
+ @TaskLocal private static var activeContext: LogBrewTraceContext?
164
+
165
+ public static var current: LogBrewTraceContext? {
166
+ activeContext
167
+ }
168
+
169
+ public static func createContext(traceFlags: String = "01") throws -> LogBrewTraceContext {
170
+ try LogBrewTraceContext.createRoot(traceFlags: traceFlags)
171
+ }
172
+
173
+ public static func openTelemetrySpanContext(
174
+ traceId: String,
175
+ spanId: String,
176
+ traceFlags: String = "01",
177
+ ) throws -> LogBrewOpenTelemetrySpanContext {
178
+ try LogBrewOpenTelemetrySpanContext(traceId: traceId, spanId: spanId, traceFlags: traceFlags)
179
+ }
180
+
181
+ public static func openTelemetrySpanContext(
182
+ traceId: String,
183
+ spanId: String,
184
+ sampled: Bool,
185
+ ) throws -> LogBrewOpenTelemetrySpanContext {
186
+ try LogBrewOpenTelemetrySpanContext(traceId: traceId, spanId: spanId, sampled: sampled)
187
+ }
188
+
189
+ public static func openTelemetrySpanContext(
190
+ from carrier: some LogBrewOpenTelemetrySpanContextCarrier,
191
+ ) throws -> LogBrewOpenTelemetrySpanContext? {
192
+ guard carrier.logBrewOpenTelemetryIsValid else {
193
+ return nil
194
+ }
195
+ return try LogBrewOpenTelemetrySpanContext(
196
+ traceId: carrier.logBrewOpenTelemetryTraceId,
197
+ spanId: carrier.logBrewOpenTelemetrySpanId,
198
+ traceFlags: carrier.logBrewOpenTelemetryTraceFlags,
199
+ )
200
+ }
201
+
202
+ public static func childContext(of parent: LogBrewTraceContext) -> LogBrewTraceContext {
203
+ LogBrewTraceContext.child(of: parent)
204
+ }
205
+
206
+ public static func context(
207
+ fromOpenTelemetrySpanContext spanContext: LogBrewOpenTelemetrySpanContext,
208
+ ) -> LogBrewTraceContext {
209
+ LogBrewTraceContext.child(
210
+ traceId: spanContext.traceId,
211
+ parentSpanId: spanContext.spanId,
212
+ traceFlags: spanContext.traceFlags,
213
+ )
214
+ }
215
+
216
+ public static func context(
217
+ fromOpenTelemetrySpanContextCarrier carrier: some LogBrewOpenTelemetrySpanContextCarrier,
218
+ ) throws -> LogBrewTraceContext? {
219
+ guard let spanContext = try openTelemetrySpanContext(from: carrier) else {
220
+ return nil
221
+ }
222
+ return context(fromOpenTelemetrySpanContext: spanContext)
223
+ }
224
+
225
+ public static func continueOrCreateContext(fromTraceparent traceparent: String?) -> LogBrewTraceContext {
226
+ guard let traceparent, !traceparent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
227
+ return LogBrewTraceContext.fallbackRoot()
228
+ }
229
+
230
+ guard let remoteParent = try? parseTraceparent(traceparent) else {
231
+ return LogBrewTraceContext.fallbackRoot()
232
+ }
233
+
234
+ return childContext(of: remoteParent)
235
+ }
236
+
237
+ public static func parseTraceparent(_ traceparent: String) throws -> LogBrewTraceContext {
238
+ let parts = traceparent.trimmingCharacters(in: .whitespacesAndNewlines).split(
239
+ separator: "-",
240
+ omittingEmptySubsequences: false,
241
+ )
242
+ guard parts.count == 4 else {
243
+ throw SdkError(
244
+ code: "validation_error",
245
+ message: "traceparent must use W3C version-traceId-parentSpanId-traceFlags format",
246
+ )
247
+ }
248
+
249
+ let version = String(parts[0]).lowercased()
250
+ guard version.count == 2, version.allSatisfy(\.isHexDigit), version != "ff" else {
251
+ throw SdkError(code: "validation_error", message: "traceparent version must be 2 hex characters and not ff")
252
+ }
253
+
254
+ let traceId = try LogBrewTraceContext.normalizedTraceId(String(parts[1]))
255
+ let parentSpanId = try LogBrewTraceContext.normalizedSpanId("traceparent parent span id", String(parts[2]))
256
+ let traceFlags = try LogBrewTraceContext.normalizedTraceFlags(String(parts[3]))
257
+ return try LogBrewTraceContext(traceId: traceId, spanId: parentSpanId, traceFlags: traceFlags)
258
+ }
259
+
260
+ public static func withContext<T>(
261
+ _ context: LogBrewTraceContext,
262
+ operation: () throws -> T,
263
+ ) rethrows -> T {
264
+ try $activeContext.withValue(context) {
265
+ try operation()
266
+ }
267
+ }
268
+
269
+ public static func withContext<T>(
270
+ _ context: LogBrewTraceContext,
271
+ operation: () async throws -> T,
272
+ ) async rethrows -> T {
273
+ try await $activeContext.withValue(context) {
274
+ try await operation()
275
+ }
276
+ }
277
+
278
+ public static func metadata(_ context: LogBrewTraceContext? = current) -> Metadata {
279
+ guard let context else {
280
+ return [:]
281
+ }
282
+
283
+ var output: Metadata = [
284
+ "traceId": .string(context.traceId),
285
+ "spanId": .string(context.spanId),
286
+ "traceFlags": .string(context.traceFlags),
287
+ "traceSampled": .bool(context.sampled),
288
+ ]
289
+ if let parentSpanId = context.parentSpanId {
290
+ output["parentSpanId"] = .string(parentSpanId)
291
+ }
292
+ return output
293
+ }
294
+
295
+ public static func metadataWithCurrentTrace(_ metadata: Metadata? = nil) -> Metadata? {
296
+ mergeTraceMetadata(metadata, context: current)
297
+ }
298
+
299
+ public static func outgoingHeaders(_ context: LogBrewTraceContext? = current) -> [String: String] {
300
+ guard let context else {
301
+ return [:]
302
+ }
303
+ return ["traceparent": context.traceparent]
304
+ }
305
+
306
+ public static func spanAttributes(
307
+ name: String,
308
+ status: SpanStatus,
309
+ durationMs: Double? = nil,
310
+ metadata: Metadata? = nil,
311
+ context: LogBrewTraceContext? = current,
312
+ ) throws -> SpanAttributes {
313
+ guard let context else {
314
+ throw SdkError(code: "validation_error", message: "trace context is required for span attributes")
315
+ }
316
+
317
+ return SpanAttributes(
318
+ name: name,
319
+ traceId: context.traceId,
320
+ spanId: context.spanId,
321
+ parentSpanId: context.parentSpanId,
322
+ status: status,
323
+ durationMs: durationMs,
324
+ metadata: mergeTraceMetadata(metadata, context: context),
325
+ )
326
+ }
327
+
328
+ public static func spanAttributesFromOpenTelemetrySpanContext(
329
+ _ spanContext: LogBrewOpenTelemetrySpanContext,
330
+ name: String,
331
+ status: SpanStatus,
332
+ durationMs: Double? = nil,
333
+ metadata: Metadata? = nil,
334
+ ) -> SpanAttributes {
335
+ let context = context(fromOpenTelemetrySpanContext: spanContext)
336
+ return SpanAttributes(
337
+ name: name,
338
+ traceId: context.traceId,
339
+ spanId: context.spanId,
340
+ parentSpanId: context.parentSpanId,
341
+ status: status,
342
+ durationMs: durationMs,
343
+ metadata: mergeTraceMetadata(metadata, context: context),
344
+ )
345
+ }
346
+
347
+ static func mergeTraceMetadata(_ metadata: Metadata?, context: LogBrewTraceContext? = current) -> Metadata? {
348
+ var output = metadata ?? [:]
349
+ for (key, value) in self.metadata(context) {
350
+ output[key] = value
351
+ }
352
+ return output.isEmpty ? nil : output
353
+ }
354
+ }
355
+
356
+ private let zeroTraceId = "00000000000000000000000000000000"
357
+ private let zeroSpanId = "0000000000000000"
@@ -0,0 +1,86 @@
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 typealias Metadata = [String: MetadataValue]
6
+
7
+ public enum MetadataValue: Codable, Equatable, Sendable {
8
+ case string(String)
9
+ case int(Int)
10
+ case double(Double)
11
+ case bool(Bool)
12
+ case null
13
+
14
+ public init(from decoder: Decoder) throws {
15
+ let container = try decoder.singleValueContainer()
16
+ if container.decodeNil() {
17
+ self = .null
18
+ } else if let value = try? container.decode(Bool.self) {
19
+ self = .bool(value)
20
+ } else if let value = try? container.decode(Int.self) {
21
+ self = .int(value)
22
+ } else if let value = try? container.decode(Double.self) {
23
+ self = .double(value)
24
+ } else {
25
+ self = try .string(container.decode(String.self))
26
+ }
27
+ }
28
+
29
+ public func encode(to encoder: Encoder) throws {
30
+ var container = encoder.singleValueContainer()
31
+ switch self {
32
+ case let .string(value):
33
+ try container.encode(value)
34
+ case let .int(value):
35
+ try container.encode(value)
36
+ case let .double(value):
37
+ try container.encode(value)
38
+ case let .bool(value):
39
+ try container.encode(value)
40
+ case .null:
41
+ try container.encodeNil()
42
+ }
43
+ }
44
+ }
45
+
46
+ extension MetadataValue: ExpressibleByStringLiteral {
47
+ public init(stringLiteral value: String) {
48
+ self = .string(value)
49
+ }
50
+ }
51
+
52
+ extension MetadataValue: ExpressibleByIntegerLiteral {
53
+ public init(integerLiteral value: Int) {
54
+ self = .int(value)
55
+ }
56
+ }
57
+
58
+ extension MetadataValue: ExpressibleByFloatLiteral {
59
+ public init(floatLiteral value: Double) {
60
+ self = .double(value)
61
+ }
62
+ }
63
+
64
+ extension MetadataValue: ExpressibleByBooleanLiteral {
65
+ public init(booleanLiteral value: Bool) {
66
+ self = .bool(value)
67
+ }
68
+ }
69
+
70
+ extension MetadataValue: ExpressibleByNilLiteral {
71
+ public init(nilLiteral _: ()) {
72
+ self = .null
73
+ }
74
+ }
75
+
76
+ func validateMetadata(_ metadata: Metadata?, label: String) throws {
77
+ guard let metadata else {
78
+ return
79
+ }
80
+ for (key, value) in metadata {
81
+ try requireNonEmpty("\(label) key", key)
82
+ if case let .double(number) = value, !number.isFinite {
83
+ throw SdkError(code: "validation_error", message: "\(label) numeric values must be finite")
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,27 @@
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
+ @_spi(CrashReplay)
6
+ public enum NativeStackArchitecture: String, Codable, Equatable, Sendable {
7
+ case arm64
8
+ case arm64e
9
+ case x8664 = "x86_64"
10
+ }
11
+
12
+ @_spi(CrashReplay)
13
+ public struct NativeStackFrame: Codable, Equatable, Sendable {
14
+ public let imageUuid: String
15
+ public let architecture: NativeStackArchitecture
16
+ public let instructionOffset: String
17
+
18
+ public init(
19
+ imageUuid: String,
20
+ architecture: NativeStackArchitecture,
21
+ instructionOffset: String,
22
+ ) {
23
+ self.imageUuid = imageUuid
24
+ self.architecture = architecture
25
+ self.instructionOffset = instructionOffset
26
+ }
27
+ }
@@ -0,0 +1,228 @@
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
+ private let productAnalyticsSchemaVersion = 1
6
+ private let maxProductAnalyticsSurfaceLength = 256
7
+
8
+ public struct ProductTimelineContext: Equatable, Sendable {
9
+ public let sessionId: String?
10
+ public let screen: String?
11
+ public let traceId: String?
12
+ public let funnel: String?
13
+ public let step: String?
14
+ public let metadata: Metadata?
15
+
16
+ public init(
17
+ sessionId: String? = nil,
18
+ screen: String? = nil,
19
+ traceId: String? = nil,
20
+ funnel: String? = nil,
21
+ step: String? = nil,
22
+ metadata: Metadata? = nil,
23
+ ) {
24
+ self.sessionId = sessionId
25
+ self.screen = screen
26
+ self.traceId = traceId
27
+ self.funnel = funnel
28
+ self.step = step
29
+ self.metadata = metadata
30
+ }
31
+ }
32
+
33
+ public extension LogBrewClient {
34
+ func captureProductAction(
35
+ _ id: String,
36
+ timestamp: String,
37
+ name: String,
38
+ status: ActionStatus = .success,
39
+ context: ProductTimelineContext = ProductTimelineContext(),
40
+ metadata: Metadata? = nil,
41
+ ) throws {
42
+ var eventMetadata = try productTimelineMetadata(
43
+ source: "swift.action",
44
+ context: context,
45
+ metadata: metadata,
46
+ )
47
+ eventMetadata["analyticsSchemaVersion"] = .int(productAnalyticsSchemaVersion)
48
+ eventMetadata["analyticsKind"] = .string("interaction")
49
+ if let surface = boundedProductAnalyticsSurface(context.screen) {
50
+ eventMetadata["analyticsSurface"] = .string(surface)
51
+ } else {
52
+ eventMetadata.removeValue(forKey: "analyticsSurface")
53
+ }
54
+ try action(
55
+ id,
56
+ timestamp: timestamp,
57
+ attributes: ActionAttributes(
58
+ name: name,
59
+ status: status,
60
+ metadata: eventMetadata,
61
+ context: timelineTelemetryContext(context),
62
+ ),
63
+ )
64
+ }
65
+
66
+ func captureNetworkMilestone(
67
+ _ id: String,
68
+ timestamp: String,
69
+ method: String,
70
+ routeTemplate: String,
71
+ statusCode: Int? = nil,
72
+ durationMs: Double? = nil,
73
+ status: ActionStatus? = nil,
74
+ context: ProductTimelineContext = ProductTimelineContext(),
75
+ metadata: Metadata? = nil,
76
+ ) throws {
77
+ let normalizedMethod = try normalizedNetworkMethod(method)
78
+ let normalizedRoute = try normalizedRouteTemplate(routeTemplate)
79
+ let checkedStatusCode = try validatedStatusCode(statusCode)
80
+ let checkedDurationMs = try validatedDurationMs(durationMs)
81
+ var eventMetadata = try productTimelineMetadata(
82
+ source: "swift.network",
83
+ context: context,
84
+ metadata: metadata,
85
+ )
86
+ eventMetadata["method"] = .string(normalizedMethod)
87
+ eventMetadata["routeTemplate"] = .string(normalizedRoute)
88
+ if let checkedStatusCode {
89
+ eventMetadata["statusCode"] = .int(checkedStatusCode)
90
+ }
91
+ if let checkedDurationMs {
92
+ eventMetadata["durationMs"] = .double(checkedDurationMs)
93
+ }
94
+
95
+ try action(
96
+ id,
97
+ timestamp: timestamp,
98
+ attributes: ActionAttributes(
99
+ name: "\(normalizedMethod) \(normalizedRoute)",
100
+ status: status ?? statusFromStatusCode(checkedStatusCode),
101
+ metadata: eventMetadata,
102
+ context: timelineTelemetryContext(context),
103
+ ),
104
+ )
105
+ }
106
+ }
107
+
108
+ private func timelineTelemetryContext(_ context: ProductTimelineContext) -> TelemetryContext? {
109
+ guard let sessionId = context.sessionId else {
110
+ return nil
111
+ }
112
+ return TelemetryContext(
113
+ session: TelemetrySessionContext(
114
+ id: sessionId.trimmingCharacters(in: .whitespacesAndNewlines),
115
+ ),
116
+ )
117
+ }
118
+
119
+ private func boundedProductAnalyticsSurface(_ surface: String?) -> String? {
120
+ guard let surface else {
121
+ return nil
122
+ }
123
+ let normalized = surface.trimmingCharacters(in: .whitespacesAndNewlines)
124
+ guard !normalized.isEmpty,
125
+ normalized.unicodeScalars.count <= maxProductAnalyticsSurfaceLength
126
+ else {
127
+ return nil
128
+ }
129
+ let hasControlCharacter = normalized.unicodeScalars.contains { scalar in
130
+ scalar.value <= 31 || (scalar.value >= 127 && scalar.value <= 159)
131
+ }
132
+ return hasControlCharacter ? nil : normalized
133
+ }
134
+
135
+ private func productTimelineMetadata(
136
+ source: String,
137
+ context: ProductTimelineContext,
138
+ metadata: Metadata?,
139
+ ) throws -> Metadata {
140
+ var output: Metadata = [:]
141
+ try copyOptionalString("sessionId", context.sessionId, into: &output)
142
+ try copyOptionalString("screen", context.screen, into: &output)
143
+ try copyOptionalString("traceId", context.traceId, into: &output)
144
+ try copyOptionalString("funnel", context.funnel, into: &output)
145
+ try copyOptionalString("step", context.step, into: &output)
146
+ try copyMetadata(context.metadata, into: &output)
147
+ try copyMetadata(metadata, into: &output)
148
+ output["source"] = .string(source)
149
+ return LogBrewTrace.mergeTraceMetadata(output) ?? output
150
+ }
151
+
152
+ private func copyOptionalString(_ key: String, _ value: String?, into output: inout Metadata) throws {
153
+ guard let value else {
154
+ return
155
+ }
156
+ let normalized = value.trimmingCharacters(in: .whitespacesAndNewlines)
157
+ try requireNonEmpty(key, normalized)
158
+ output[key] = .string(normalized)
159
+ }
160
+
161
+ func copyMetadata(_ metadata: Metadata?, into output: inout Metadata) throws {
162
+ guard let metadata else {
163
+ return
164
+ }
165
+ for (key, value) in metadata {
166
+ try requireNonEmpty("metadata key", key)
167
+ if case let .double(doubleValue) = value, !doubleValue.isFinite {
168
+ throw SdkError(code: "validation_error", message: "metadata value for \(key) must be finite")
169
+ }
170
+ output[key] = value
171
+ }
172
+ }
173
+
174
+ func normalizedNetworkMethod(_ method: String) throws -> String {
175
+ try requireNonEmpty("network method", method)
176
+ return method.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
177
+ }
178
+
179
+ func normalizedRouteTemplate(_ routeTemplate: String) throws -> String {
180
+ try requireNonEmpty("network routeTemplate", routeTemplate)
181
+ let trimmed = routeTemplate.trimmingCharacters(in: .whitespacesAndNewlines)
182
+ if let components = URLComponents(string: trimmed), components.scheme == "http" || components.scheme == "https" {
183
+ let path = components.path.isEmpty ? "/" : components.path
184
+ return stripQueryAndHash(path)
185
+ }
186
+ if trimmed.contains("://") {
187
+ throw SdkError(
188
+ code: "validation_error",
189
+ message: "network routeTemplate must be a route template or HTTP(S) URL",
190
+ )
191
+ }
192
+ let sanitized = stripQueryAndHash(trimmed)
193
+ try requireNonEmpty("network routeTemplate", sanitized)
194
+ return sanitized
195
+ }
196
+
197
+ private func stripQueryAndHash(_ value: String) -> String {
198
+ let withoutQuery = value.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: false).first ?? ""
199
+ let withoutHash = withoutQuery.split(separator: "#", maxSplits: 1, omittingEmptySubsequences: false).first ?? ""
200
+ return String(withoutHash)
201
+ }
202
+
203
+ func validatedStatusCode(_ statusCode: Int?) throws -> Int? {
204
+ guard let statusCode else {
205
+ return nil
206
+ }
207
+ guard (100 ... 599).contains(statusCode) else {
208
+ throw SdkError(code: "validation_error", message: "network statusCode must be between 100 and 599")
209
+ }
210
+ return statusCode
211
+ }
212
+
213
+ func validatedDurationMs(_ durationMs: Double?) throws -> Double? {
214
+ guard let durationMs else {
215
+ return nil
216
+ }
217
+ guard durationMs.isFinite, durationMs >= 0 else {
218
+ throw SdkError(code: "validation_error", message: "network durationMs must be finite and non-negative")
219
+ }
220
+ return durationMs
221
+ }
222
+
223
+ func statusFromStatusCode(_ statusCode: Int?) -> ActionStatus {
224
+ guard let statusCode else {
225
+ return .success
226
+ }
227
+ return statusCode >= 400 ? .failure : .success
228
+ }