import Foundation import GRDB public struct CapturedTraffic: Codable, FetchableRecord, MutablePersistableRecord, Identifiable, Sendable { public var id: Int64? public var requestId: String public var domain: String public var url: String public var method: String public var scheme: String public var statusCode: Int? public var statusText: String? // Request public var requestHeaders: String? public var requestBody: Data? public var requestBodySize: Int public var requestContentType: String? public var queryParameters: String? // Response public var responseHeaders: String? public var responseBody: Data? public var responseBodySize: Int public var responseContentType: String? // Timing public var startedAt: Double public var completedAt: Double? public var durationMs: Int? // Metadata public var isSslDecrypted: Bool public var isPinned: Bool public var isWebsocket: Bool public var isHidden: Bool public var createdAt: Double public static let databaseTableName = "captured_traffic" public mutating func didInsert(_ inserted: InsertionSuccess) { id = inserted.rowID } public init( id: Int64? = nil, requestId: String = UUID().uuidString, domain: String, url: String, method: String, scheme: String, statusCode: Int? = nil, statusText: String? = nil, requestHeaders: String? = nil, requestBody: Data? = nil, requestBodySize: Int = 0, requestContentType: String? = nil, queryParameters: String? = nil, responseHeaders: String? = nil, responseBody: Data? = nil, responseBodySize: Int = 0, responseContentType: String? = nil, startedAt: Double = Date().timeIntervalSince1970, completedAt: Double? = nil, durationMs: Int? = nil, isSslDecrypted: Bool = false, isPinned: Bool = false, isWebsocket: Bool = false, isHidden: Bool = false, createdAt: Double = Date().timeIntervalSince1970 ) { self.id = id self.requestId = requestId self.domain = domain self.url = url self.method = method self.scheme = scheme self.statusCode = statusCode self.statusText = statusText self.requestHeaders = requestHeaders self.requestBody = requestBody self.requestBodySize = requestBodySize self.requestContentType = requestContentType self.queryParameters = queryParameters self.responseHeaders = responseHeaders self.responseBody = responseBody self.responseBodySize = responseBodySize self.responseContentType = responseContentType self.startedAt = startedAt self.completedAt = completedAt self.durationMs = durationMs self.isSslDecrypted = isSslDecrypted self.isPinned = isPinned self.isWebsocket = isWebsocket self.isHidden = isHidden self.createdAt = createdAt } } // MARK: - Computed Properties extension CapturedTraffic { public var decodedRequestHeaders: [String: String] { guard let data = requestHeaders?.data(using: .utf8), let dict = try? JSONDecoder().decode([String: String].self, from: data) else { return [:] } return dict } public var decodedResponseHeaders: [String: String] { guard let data = responseHeaders?.data(using: .utf8), let dict = try? JSONDecoder().decode([String: String].self, from: data) else { return [:] } return dict } public var decodedQueryParameters: [String: String] { guard let data = queryParameters?.data(using: .utf8), let dict = try? JSONDecoder().decode([String: String].self, from: data) else { return [:] } return dict } public var startDate: Date { Date(timeIntervalSince1970: startedAt) } public var formattedDuration: String { guard let ms = durationMs else { return "-" } if ms < 1000 { return "\(ms) ms" } else { return String(format: "%.1f s", Double(ms) / 1000.0) } } }