Skip to content

Commit

Permalink
Merge pull request #236 from jjotaum/feature/export-session-as-har
Browse files Browse the repository at this point in the history
Export session logs as HAR file.
  • Loading branch information
kean authored Feb 18, 2024
2 parents e4fc7cc + c96d0ca commit 4b57345
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 17 deletions.
6 changes: 5 additions & 1 deletion Pulse.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 53;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -271,6 +271,7 @@
0CFF79DD29EB7A8E00BE767B /* SessionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFF79DB29EB7A8B00BE767B /* SessionListView.swift */; };
0CFF79DF29EB7B4300BE767B /* SessionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFF79DE29EB7B4300BE767B /* SessionPickerView.swift */; };
0CFF79E229EC1FA200BE767B /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFF79E029EC1F7800BE767B /* ImageProcessor.swift */; };
E95D6C562B7314E6004D28E4 /* HARDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D6C552B7314E6004D28E4 /* HARDocument.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -727,6 +728,7 @@
0CFF79E029EC1F7800BE767B /* ImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = "<group>"; };
49E82A8526D107A00070244F /* AlamofireIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlamofireIntegration.swift; sourceTree = "<group>"; };
49E82A8826D1083D0070244F /* MoyaIntegration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoyaIntegration.swift; sourceTree = "<group>"; };
E95D6C552B7314E6004D28E4 /* HARDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HARDocument.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -1186,6 +1188,7 @@
0CB17F192978ABBA004E33F4 /* ManagedObjectsCountObserver.swift */,
0C3002B22986CEF30055F6C2 /* LoggerStoreIndex.swift */,
0C1050E429E1FAC1006AFDAD /* Version.swift */,
E95D6C552B7314E6004D28E4 /* HARDocument.swift */,
);
path = Helpers;
sourceTree = "<group>";
Expand Down Expand Up @@ -2012,6 +2015,7 @@
0C7A0E00297C51CE00B4B69D /* ConsoleListOptions.swift in Sources */,
0CF0D668296F189600EED9D4 /* NetworkRequestStatusCell.swift in Sources */,
0C9751562A32CBFB00DC46FF /* RemoteLoggerSelectedDeviceView.swift in Sources */,
E95D6C562B7314E6004D28E4 /* HARDocument.swift in Sources */,
0CB63A2F2975C43D00525165 /* ConsoleSearchScope.swift in Sources */,
0CF0D640296F189600EED9D4 /* ConsoleFilterViewModel.swift in Sources */,
0CF0D65A296F189600EED9D4 /* StoreDetailsView.swift in Sources */,
Expand Down
282 changes: 282 additions & 0 deletions Sources/PulseUI/Helpers/HARDocument.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
//
// HARDocument.swift
// PulseUI
//
// Created by Jota Uribe on 6/02/24.
// Copyright © 2024 kean. All rights reserved.
//

import Foundation
import Pulse

fileprivate enum HARDateFormatter {
static var formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFullDate, .withFullTime]
return formatter
}()
}

struct HARDocument: Encodable {
private let log: Log
init(store: LoggerStore) throws {
var entries: [Entry] = []
var pages: [Page] = []
try Dictionary(grouping: store.allTasks(), by: \.url).values.forEach { networkTasks in
let pageId = "page_\(pages.count)"
pages.append(
.init(
id: pageId,
startedDateTime: HARDateFormatter.formatter.string(from: networkTasks[.zero].createdAt),
title: networkTasks[.zero].url ?? ""
)
)
entries.append(contentsOf: networkTasks.map { .init(entity: $0, pageId: pageId) })
}
try store.allMessages().forEach { message in
if let task = message.task {
entries.append(.init(entity: task, pageId: "page_\(pages.count)"))
}
}
log = .init(
version: "1.2",
creator: ["name": "Pulse HAR generation tool", "version": "0.1"],
pages: pages,
entries: entries
)
}
}

extension HARDocument {
struct Log: Encodable {
let version: String
let creator: [String: String]
var pages: [Page]
var entries: [Entry]
}

struct Page: Encodable {
let id: String
let pageTimings: PageTimings
let startedDateTime: String?
let title: String

init(
id: String,
pageTimings: PageTimings = .init(),
startedDateTime: String,
title: String
) {
self.id = id
self.pageTimings = pageTimings
self.startedDateTime = startedDateTime
self.title = title
}
}

struct Entry: Encodable {
let cache: Cache
let connection: String
let pageref: String
let request: Request
let response: Response?
let serverIPAddress: String
let startedDateTime: String
let time: Double
let timings: Timings?

init(entity: NetworkTaskEntity, pageId: String) {
cache = .init()
connection = "\(entity.orderedTransactions.first?.remotePort ?? .zero)"
pageref = pageId
request = .init(
cookies: [],
headers: entity.originalRequest?.headers.compactMap { ["name": $0.key, "value": $0.value] } ?? [],
httpVersion: "HTTP/2",
method: entity.httpMethod,
queryString: [],
url: entity.url
)

response = .init(entity)

serverIPAddress = entity.orderedTransactions.first?.remoteAddress ?? ""
startedDateTime = HARDateFormatter.formatter.string(from: entity.createdAt)
time = entity.duration * 1000
timings = .init(entity.orderedTransactions.last?.timing)
}
}

struct Timings: Encodable {
let blocked: Double
let connect: Int
let dns: Int
let receive: Double
let send: Double
let ssl: Int
let wait: Double

init?(_ timing: NetworkLogger.TransactionTimingInfo?) {
if let timing {
blocked = -1
connect = Self.millisecondsBetween(
startDate: timing.fetchStartDate,
endDate: timing.connectEndDate
)

dns = Self.millisecondsBetween(
startDate: timing.domainLookupStartDate,
endDate: timing.domainLookupEndDate
)

receive = Self.intervalBetween(
startDate: timing.responseStartDate,
endDate: timing.responseEndDate
)

send = Self.intervalBetween(
startDate: timing.requestStartDate,
endDate: timing.requestEndDate
)

ssl = Self.millisecondsBetween(
startDate: timing.secureConnectionStartDate,
endDate: timing.secureConnectionEndDate
)

wait = timing.duration ?? .zero
} else {
return nil
}
}
}

struct PageTimings: Encodable {
let onContentLoad: Int
let onLoad: Int

init(
onContentLoad: Int = -1,
onLoad: Int = -1
) {
self.onContentLoad = onContentLoad
self.onLoad = onLoad
}
}
}

extension HARDocument.Entry {
struct Request: Encodable {
var bodySize: Int = -1
let cookies: [[String: String]]
let headers: [[String: String]]
let httpVersion: String
let method: String?
let queryString: [[String: String]]
let url: String?
}

struct Response: Encodable {
let bodySize: Int
let content: Content?
let cookies: [[String: String]]
let headers: [[String: String]]
let headersSize: Int
let httpVersion: String
let redirectURL: String
let status: Int
var statusText: String

init?(_ entity: NetworkTaskEntity?) {
if let entity {
bodySize = Int(entity.responseBody?.size ?? -1)
content = .init(entity.responseBody)
cookies = []
headers = entity.response?.headers.compactMap { ["name": $0.key, "value": $0.value] } ?? []
headersSize = -1
httpVersion = "HTTP/2"
redirectURL = ""
status = Int(entity.statusCode)
statusText = ""
} else {
return nil
}
}
}

struct Content: Encodable {
let compression: Int
let encoding: String?
let mimeType: String
let size: Int
var text: String = ""

init?(_ entity: LoggerBlobHandleEntity?) {
if let entity {
compression = Int(entity.size - entity.decompressedSize)
encoding = ""
mimeType = entity.contentType?.rawValue ?? ""
size = entity.data?.count ?? .zero
if let data = entity.data {
text = String(decoding: data, as: UTF8.self)
}
} else {
return nil
}
}
}

struct Cache: Encodable {
let afterRequest: Item?
let beforeRequest: Item?

init(
afterRequest: Item? = nil,
beforeRequest: Item? = nil
) {
self.afterRequest = afterRequest
self.beforeRequest = beforeRequest
}
}
}

extension HARDocument.Entry.Cache {
struct Item: Encodable {
let eTag: String
let expires: String
let hitCount: Int
let lastAccess: String

init(
eTag: String = "",
expires: String = "",
hitCount: Int = .zero,
lastAccess: String = ""
) {
self.eTag = eTag
self.expires = expires
self.hitCount = hitCount
self.lastAccess = lastAccess
}
}
}

// MARK: - Helper Methods

extension HARDocument.Timings {
fileprivate static func millisecondsBetween(startDate: Date?, endDate: Date?) -> Int {
let timeInterval = intervalBetween(startDate: startDate, endDate: endDate)
guard timeInterval != .zero else {
// return -1 if value can not be determined as indicated on HAR document specs.
return -1
}
return Int(timeInterval * 1000)
}

fileprivate static func intervalBetween(startDate: Date?, endDate: Date?) -> TimeInterval {
guard let startDate, let endDate else {
return .zero
}
return endDate.timeIntervalSince(startDate)
}
}
10 changes: 9 additions & 1 deletion Sources/PulseUI/Helpers/ShareItems.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import Pulse
import CoreData

public enum ShareStoreOutput: String, RawRepresentable {
case store, package, text, html
case store, package, text, html, har

var fileExtension: String {
switch self {
case .store, .package: return "pulse"
case .text: return "txt"
case .html: return "html"
case .har: return "har"
}
}
}
Expand Down Expand Up @@ -76,6 +77,11 @@ enum ShareService {
#else
return ShareItems(["Sharing as PDF is not supported on this platform"])
#endif
case .har:
let har = TextUtilities.har(from: string)
let directory = TemporaryDirectory()
let fileURL = directory.write(data: har, extension: "har")
return ShareItems([fileURL], size: Int64(har.count), cleanup: directory.remove)
}
}

Expand Down Expand Up @@ -110,12 +116,14 @@ enum ShareOutput {
case plainText
case html
case pdf
case har

var title: String {
switch self {
case .plainText: return "Text"
case .html: return "HTML"
case .pdf: return "PDF"
case .har: return "HAR"
}
}
}
Expand Down
Loading

0 comments on commit 4b57345

Please sign in to comment.