import Foundation final class RequestTiming: NSObject, URLSessionTaskDelegate, Sendable { private static let slow: TimeInterval = 0.2 func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { guard let t = metrics.transactionMetrics.last, let response = t.response as? HTTPURLResponse, response.statusCode != 101, response.value(forHTTPHeaderField: "Content-Type") != "text/event-stream", let fetched = t.fetchStartDate, let sent = t.requestStartDate, let first = t.responseStartDate, let done = t.responseEndDate else { return } let finished = Date() let serverTiming = response.value(forHTTPHeaderField: "Server-Timing") ?? "" let server = Self.serverTotal(serverTiming) let queue = (t.connectStartDate ?? sent).timeIntervalSince(fetched) let connect = t.connectStartDate.flatMap { start in t.connectEndDate.map { $0.timeIntervalSince(start) } } ?? 0 let network = first.timeIntervalSince(sent) - server let download = done.timeIntervalSince(first) let total = done.timeIntervalSince(fetched) let line = "\(task.originalRequest?.httpMethod ?? "") \(task.originalRequest?.url?.path ?? "")" Task { @MainActor in let app = Date().timeIntervalSince(finished) if total + app < Self.slow { return } log.notice("request slow \(line, privacy: .public) total=\(Self.ms(total + app), privacy: .public) queue=\(Self.ms(queue), privacy: .public) connect=\(Self.ms(connect), privacy: .public) network=\(Self.ms(network), privacy: .public) server=\(Self.ms(server), privacy: .public) download=\(Self.ms(download), privacy: .public) app=\(Self.ms(app), privacy: .public) spans=\(serverTiming, privacy: .public)") } } private static func serverTotal(_ header: String) -> TimeInterval { for metric in header.split(separator: ",") { let parts = metric.split(separator: ";").map { $0.trimmingCharacters(in: .whitespaces) } if parts.first == "total", let dur = parts.dropFirst().first(where: { $0.hasPrefix("dur=") }), let value = Double(dur.dropFirst(4)) { return value / 1000 } } return 0 } private static func ms(_ seconds: TimeInterval) -> String { "\(Int((seconds * 1000).rounded()))ms" } }