@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.
- package/LogBrewReactNative.podspec +25 -3
- package/README.md +106 -3
- 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 +84 -2
- package/index.d.cts +4 -0
- package/index.d.ts +4 -0
- package/index.js +84 -2
- package/index.native.d.ts +4 -0
- package/index.native.js +14 -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/package.json +43 -5
- package/promise-rejections.cjs +3 -1
- package/scripts/sync-apple-native-sources.mjs +93 -0
- 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
|
+
}
|